Pull request: hup-reload
Merge in DNS/adguard-home from hup-reload to master Squashed commit of the following: commit 5cd4ab85bdc7544a4eded2a61f5a5571175daa44 Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Fri Oct 7 19:58:17 2022 +0300 next: imp signal hdlr commit 8fd18e749fec46982d26fc408e661bd802586c37 Merge: a8780455f1dd3334
Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Fri Oct 7 19:46:48 2022 +0300 Merge branch 'master' into hup-reload commit a87804550e15d7fe3d9ded2e5a736c395f96febd Merge: 349dbe54960a7a75
Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Fri Oct 7 15:49:23 2022 +0300 Merge branch 'master' into hup-reload commit 349dbe54fe27eeaf56776c73c3cc5649018d4c60 Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Fri Oct 7 15:43:52 2022 +0300 next: imp docs, names commit 7287a86d283489127453009267911003cea5227e Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Fri Oct 7 13:39:44 2022 +0300 WIP all: impl dynamic reconfiguration
This commit is contained in:
parent
f1dd33346a
commit
f5602d9c46
|
@ -15,11 +15,11 @@ import (
|
||||||
// errFSOpen.
|
// errFSOpen.
|
||||||
type errFS struct{}
|
type errFS struct{}
|
||||||
|
|
||||||
// errFSOpen is returned from errGlobFS.Open.
|
// errFSOpen is returned from errFS.Open.
|
||||||
const errFSOpen errors.Error = "test open error"
|
const errFSOpen errors.Error = "test open error"
|
||||||
|
|
||||||
// Open implements the fs.FS interface for *errGlobFS. fsys is always nil and
|
// Open implements the fs.FS interface for *errFS. fsys is always nil and err
|
||||||
// err is always errFSOpen.
|
// is always errFSOpen.
|
||||||
func (efs *errFS) Open(name string) (fsys fs.File, err error) {
|
func (efs *errFS) Open(name string) (fsys fs.File, err error) {
|
||||||
return nil, errFSOpen
|
return nil, errFSOpen
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,11 +175,21 @@ func RootDirFS() (fsys fs.FS) {
|
||||||
return os.DirFS("")
|
return os.DirFS("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotifyReconfigureSignal notifies c on receiving reconfigure signals.
|
||||||
|
func NotifyReconfigureSignal(c chan<- os.Signal) {
|
||||||
|
notifyReconfigureSignal(c)
|
||||||
|
}
|
||||||
|
|
||||||
// NotifyShutdownSignal notifies c on receiving shutdown signals.
|
// NotifyShutdownSignal notifies c on receiving shutdown signals.
|
||||||
func NotifyShutdownSignal(c chan<- os.Signal) {
|
func NotifyShutdownSignal(c chan<- os.Signal) {
|
||||||
notifyShutdownSignal(c)
|
notifyShutdownSignal(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsReconfigureSignal returns true if sig is a reconfigure signal.
|
||||||
|
func IsReconfigureSignal(sig os.Signal) (ok bool) {
|
||||||
|
return isReconfigureSignal(sig)
|
||||||
|
}
|
||||||
|
|
||||||
// IsShutdownSignal returns true if sig is a shutdown signal.
|
// IsShutdownSignal returns true if sig is a shutdown signal.
|
||||||
func IsShutdownSignal(sig os.Signal) (ok bool) {
|
func IsShutdownSignal(sig os.Signal) (ok bool) {
|
||||||
return isShutdownSignal(sig)
|
return isShutdownSignal(sig)
|
||||||
|
|
|
@ -9,10 +9,18 @@ import (
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func notifyReconfigureSignal(c chan<- os.Signal) {
|
||||||
|
signal.Notify(c, unix.SIGHUP)
|
||||||
|
}
|
||||||
|
|
||||||
func notifyShutdownSignal(c chan<- os.Signal) {
|
func notifyShutdownSignal(c chan<- os.Signal) {
|
||||||
signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM)
|
signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isReconfigureSignal(sig os.Signal) (ok bool) {
|
||||||
|
return sig == unix.SIGHUP
|
||||||
|
}
|
||||||
|
|
||||||
func isShutdownSignal(sig os.Signal) (ok bool) {
|
func isShutdownSignal(sig os.Signal) (ok bool) {
|
||||||
switch sig {
|
switch sig {
|
||||||
case
|
case
|
||||||
|
|
|
@ -39,12 +39,20 @@ func isOpenWrt() (ok bool) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notifyReconfigureSignal(c chan<- os.Signal) {
|
||||||
|
signal.Notify(c, windows.SIGHUP)
|
||||||
|
}
|
||||||
|
|
||||||
func notifyShutdownSignal(c chan<- os.Signal) {
|
func notifyShutdownSignal(c chan<- os.Signal) {
|
||||||
// syscall.SIGTERM is processed automatically. See go doc os/signal,
|
// syscall.SIGTERM is processed automatically. See go doc os/signal,
|
||||||
// section Windows.
|
// section Windows.
|
||||||
signal.Notify(c, os.Interrupt)
|
signal.Notify(c, os.Interrupt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isReconfigureSignal(sig os.Signal) (ok bool) {
|
||||||
|
return sig == windows.SIGHUP
|
||||||
|
}
|
||||||
|
|
||||||
func isShutdownSignal(sig os.Signal) (ok bool) {
|
func isShutdownSignal(sig os.Signal) (ok bool) {
|
||||||
switch sig {
|
switch sig {
|
||||||
case
|
case
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
@ -88,7 +89,7 @@ func (l *Listener) Close() (err error) {
|
||||||
return l.OnClose()
|
return l.OnClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module AdGuardHome
|
// Module adguard-home
|
||||||
|
|
||||||
// Package aghos
|
// Package aghos
|
||||||
|
|
||||||
|
@ -117,29 +118,31 @@ func (w *FSWatcher) Close() (err error) {
|
||||||
return w.OnClose()
|
return w.OnClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package websvc
|
// Package agh
|
||||||
|
|
||||||
// ServiceWithConfig is a mock [websvc.ServiceWithConfig] implementation for
|
// type check
|
||||||
// tests.
|
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
|
||||||
|
|
||||||
|
// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
|
||||||
type ServiceWithConfig[ConfigType any] struct {
|
type ServiceWithConfig[ConfigType any] struct {
|
||||||
OnStart func() (err error)
|
OnStart func() (err error)
|
||||||
OnShutdown func(ctx context.Context) (err error)
|
OnShutdown func(ctx context.Context) (err error)
|
||||||
OnConfig func() (c ConfigType)
|
OnConfig func() (c ConfigType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start implements the [websvc.ServiceWithConfig] interface for
|
// Start implements the [agh.ServiceWithConfig] interface for
|
||||||
// *ServiceWithConfig.
|
// *ServiceWithConfig.
|
||||||
func (s *ServiceWithConfig[_]) Start() (err error) {
|
func (s *ServiceWithConfig[_]) Start() (err error) {
|
||||||
return s.OnStart()
|
return s.OnStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown implements the [websvc.ServiceWithConfig] interface for
|
// Shutdown implements the [agh.ServiceWithConfig] interface for
|
||||||
// *ServiceWithConfig.
|
// *ServiceWithConfig.
|
||||||
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
|
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
|
||||||
return s.OnShutdown(ctx)
|
return s.OnShutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config implements the [websvc.ServiceWithConfig] interface for
|
// Config implements the [agh.ServiceWithConfig] interface for
|
||||||
// *ServiceWithConfig.
|
// *ServiceWithConfig.
|
||||||
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||||
return s.OnConfig()
|
return s.OnConfig()
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
package aghtest_test
|
package aghtest_test
|
||||||
|
|
||||||
import (
|
// Put interface checks that cause import cycles here.
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ websvc.ServiceWithConfig[struct{}] = (*aghtest.ServiceWithConfig[struct{}])(nil)
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
// Package agh contains common entities and interfaces of AdGuard Home.
|
// Package agh contains common entities and interfaces of AdGuard Home.
|
||||||
//
|
|
||||||
// TODO(a.garipov): Move to the upper-level internal/.
|
|
||||||
package agh
|
package agh
|
||||||
|
|
||||||
import "context"
|
import "context"
|
||||||
|
@ -23,11 +21,43 @@ type Service interface {
|
||||||
// type check
|
// type check
|
||||||
var _ Service = EmptyService{}
|
var _ Service = EmptyService{}
|
||||||
|
|
||||||
// EmptyService is a Service that does nothing.
|
// EmptyService is a [Service] that does nothing.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Remove if unnecessary.
|
||||||
type EmptyService struct{}
|
type EmptyService struct{}
|
||||||
|
|
||||||
// Start implements the Service interface for EmptyService.
|
// Start implements the [Service] interface for EmptyService.
|
||||||
func (EmptyService) Start() (err error) { return nil }
|
func (EmptyService) Start() (err error) { return nil }
|
||||||
|
|
||||||
// Shutdown implements the Service interface for EmptyService.
|
// Shutdown implements the [Service] interface for EmptyService.
|
||||||
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
||||||
|
|
||||||
|
// ServiceWithConfig is an extension of the [Service] interface for services
|
||||||
|
// that can return their configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
||||||
|
// how to make it testable in a better way.
|
||||||
|
type ServiceWithConfig[ConfigType any] interface {
|
||||||
|
Service
|
||||||
|
|
||||||
|
Config() (c ConfigType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)
|
||||||
|
|
||||||
|
// EmptyServiceWithConfig is a ServiceWithConfig that does nothing. Its Config
|
||||||
|
// method returns Conf.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Remove if unnecessary.
|
||||||
|
type EmptyServiceWithConfig[ConfigType any] struct {
|
||||||
|
EmptyService
|
||||||
|
|
||||||
|
Conf ConfigType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config implements the [ServiceWithConfig] interface for
|
||||||
|
// *EmptyServiceWithConfig.
|
||||||
|
func (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {
|
||||||
|
return s.Conf
|
||||||
|
}
|
||||||
|
|
|
@ -8,10 +8,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/netip"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,26 +25,32 @@ func Main(clientBuildFS fs.FS) {
|
||||||
|
|
||||||
// TODO(a.garipov): Set up logging.
|
// TODO(a.garipov): Set up logging.
|
||||||
|
|
||||||
|
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
|
||||||
|
|
||||||
// Web Service
|
// Web Service
|
||||||
|
|
||||||
// TODO(a.garipov): Use in the Web service.
|
// TODO(a.garipov): Use in the Web service.
|
||||||
_ = clientBuildFS
|
_ = clientBuildFS
|
||||||
|
|
||||||
// TODO(a.garipov): Make configurable.
|
// TODO(a.garipov): Set up configuration file name.
|
||||||
web := websvc.New(&websvc.Config{
|
const confFile = "AdGuardHome.1.yaml"
|
||||||
// TODO(a.garipov): Use an actual implementation.
|
|
||||||
ConfigManager: nil,
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
|
|
||||||
Start: start,
|
|
||||||
Timeout: 60 * time.Second,
|
|
||||||
ForceHTTPS: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
err := web.Start()
|
confMgr, err := configmgr.New(confFile, start)
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
web := confMgr.Web()
|
||||||
|
err = web.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
dns := confMgr.DNS()
|
||||||
|
err = dns.Start()
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
sigHdlr := newSignalHandler(
|
sigHdlr := newSignalHandler(
|
||||||
|
confFile,
|
||||||
|
start,
|
||||||
web,
|
web,
|
||||||
|
dns,
|
||||||
)
|
)
|
||||||
|
|
||||||
go sigHdlr.handle()
|
go sigHdlr.handle()
|
||||||
|
|
|
@ -2,18 +2,26 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// signalHandler processes incoming signals and shuts services down.
|
// signalHandler processes incoming signals and shuts services down.
|
||||||
type signalHandler struct {
|
type signalHandler struct {
|
||||||
|
// signal is the channel to which OS signals are sent.
|
||||||
signal chan os.Signal
|
signal chan os.Signal
|
||||||
|
|
||||||
// services are the services that are shut down before application
|
// confFile is the path to the configuration file.
|
||||||
// exiting.
|
confFile string
|
||||||
|
|
||||||
|
// start is the time at which AdGuard Home has been started.
|
||||||
|
start time.Time
|
||||||
|
|
||||||
|
// services are the services that are shut down before application exiting.
|
||||||
services []agh.Service
|
services []agh.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,12 +32,51 @@ func (h *signalHandler) handle() {
|
||||||
for sig := range h.signal {
|
for sig := range h.signal {
|
||||||
log.Info("sighdlr: received signal %q", sig)
|
log.Info("sighdlr: received signal %q", sig)
|
||||||
|
|
||||||
if aghos.IsShutdownSignal(sig) {
|
if aghos.IsReconfigureSignal(sig) {
|
||||||
h.shutdown()
|
h.reconfigure()
|
||||||
|
} else if aghos.IsShutdownSignal(sig) {
|
||||||
|
status := h.shutdown()
|
||||||
|
log.Info("sighdlr: exiting with status %d", status)
|
||||||
|
|
||||||
|
os.Exit(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reconfigure rereads the configuration file and updates and restarts services.
|
||||||
|
func (h *signalHandler) reconfigure() {
|
||||||
|
log.Info("sighdlr: reconfiguring adguard home")
|
||||||
|
|
||||||
|
status := h.shutdown()
|
||||||
|
if status != statusSuccess {
|
||||||
|
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
|
||||||
|
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): This is a very rough way to do it. Some services can be
|
||||||
|
// reconfigured without the full shutdown, and the error handling is
|
||||||
|
// currently not the best.
|
||||||
|
|
||||||
|
confMgr, err := configmgr.New(h.confFile, h.start)
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
web := confMgr.Web()
|
||||||
|
err = web.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
dns := confMgr.DNS()
|
||||||
|
err = dns.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
h.services = []agh.Service{
|
||||||
|
dns,
|
||||||
|
web,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("sighdlr: successfully reconfigured adguard home")
|
||||||
|
}
|
||||||
|
|
||||||
// Exit status constants.
|
// Exit status constants.
|
||||||
const (
|
const (
|
||||||
statusSuccess = 0
|
statusSuccess = 0
|
||||||
|
@ -37,11 +84,11 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// shutdown gracefully shuts down all services.
|
// shutdown gracefully shuts down all services.
|
||||||
func (h *signalHandler) shutdown() {
|
func (h *signalHandler) shutdown() (status int) {
|
||||||
ctx, cancel := ctxWithDefaultTimeout()
|
ctx, cancel := ctxWithDefaultTimeout()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
status := statusSuccess
|
status = statusSuccess
|
||||||
|
|
||||||
log.Info("sighdlr: shutting down services")
|
log.Info("sighdlr: shutting down services")
|
||||||
for i, service := range h.services {
|
for i, service := range h.services {
|
||||||
|
@ -52,19 +99,20 @@ func (h *signalHandler) shutdown() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("sighdlr: shutting down adguard home")
|
return status
|
||||||
|
|
||||||
os.Exit(status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
||||||
func newSignalHandler(svcs ...agh.Service) (h *signalHandler) {
|
func newSignalHandler(confFile string, start time.Time, svcs ...agh.Service) (h *signalHandler) {
|
||||||
h = &signalHandler{
|
h = &signalHandler{
|
||||||
signal: make(chan os.Signal, 1),
|
signal: make(chan os.Signal, 1),
|
||||||
|
confFile: confFile,
|
||||||
|
start: start,
|
||||||
services: svcs,
|
services: svcs,
|
||||||
}
|
}
|
||||||
|
|
||||||
aghos.NotifyShutdownSignal(h.signal)
|
aghos.NotifyShutdownSignal(h.signal)
|
||||||
|
aghos.NotifyReconfigureSignal(h.signal)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package configmgr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration Structures
|
||||||
|
|
||||||
|
// config is the top-level on-disk configuration structure.
|
||||||
|
type config struct {
|
||||||
|
DNS *dnsConfig `yaml:"dns"`
|
||||||
|
HTTP *httpConfig `yaml:"http"`
|
||||||
|
// TODO(a.garipov): Use.
|
||||||
|
SchemaVersion int `yaml:"schema_version"`
|
||||||
|
// TODO(a.garipov): Use.
|
||||||
|
DebugPprof bool `yaml:"debug_pprof"`
|
||||||
|
Verbose bool `yaml:"verbose"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsConfig is the on-disk DNS configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Validate.
|
||||||
|
type dnsConfig struct {
|
||||||
|
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||||
|
BootstrapDNS []string `yaml:"bootstrap_dns"`
|
||||||
|
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||||
|
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpConfig is the on-disk web API configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Validate.
|
||||||
|
type httpConfig struct {
|
||||||
|
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
|
||||||
|
Timeout timeutil.Duration `yaml:"timeout"`
|
||||||
|
ForceHTTPS bool `yaml:"force_https"`
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
// Package configmgr defines the AdGuard Home on-disk configuration entities and
|
||||||
|
// configuration manager.
|
||||||
|
package configmgr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration Manager
|
||||||
|
|
||||||
|
// Manager handles full and partial changes in the configuration, persisting
|
||||||
|
// them to disk if necessary.
|
||||||
|
type Manager struct {
|
||||||
|
// updMu makes sure that at most one reconfiguration is performed at a time.
|
||||||
|
// updMu protects all fields below.
|
||||||
|
updMu *sync.RWMutex
|
||||||
|
|
||||||
|
// dns is the DNS service.
|
||||||
|
dns *dnssvc.Service
|
||||||
|
|
||||||
|
// Web is the Web API service.
|
||||||
|
web *websvc.Service
|
||||||
|
|
||||||
|
// current is the current configuration.
|
||||||
|
current *config
|
||||||
|
|
||||||
|
// fileName is the name of the configuration file.
|
||||||
|
fileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new *Manager that persists changes to the file pointed to by
|
||||||
|
// fileName. It reads the configuration file and populates the service fields.
|
||||||
|
// start is the startup time of AdGuard Home.
|
||||||
|
func New(fileName string, start time.Time) (m *Manager, err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "reading config") }()
|
||||||
|
|
||||||
|
conf := &config{}
|
||||||
|
f, err := os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { err = errors.WithDeferred(err, f.Close()) }()
|
||||||
|
|
||||||
|
err = yaml.NewDecoder(f).Decode(conf)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Move into a separate function and add other logging
|
||||||
|
// settings.
|
||||||
|
if conf.Verbose {
|
||||||
|
log.SetLevel(log.DEBUG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate the configuration structure. Return an error
|
||||||
|
// if it's incorrect.
|
||||||
|
|
||||||
|
m = &Manager{
|
||||||
|
updMu: &sync.RWMutex{},
|
||||||
|
current: conf,
|
||||||
|
fileName: fileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Get the context with the timeout from the arguments?
|
||||||
|
const assemblyTimeout = 5 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = m.assemble(ctx, conf, start)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assemble creates all services and puts them into the corresponding fields.
|
||||||
|
// The fields of conf must not be modified after calling assemble.
|
||||||
|
func (m *Manager) assemble(ctx context.Context, conf *config, start time.Time) (err error) {
|
||||||
|
dnsConf := &dnssvc.Config{
|
||||||
|
Addresses: conf.DNS.Addresses,
|
||||||
|
BootstrapServers: conf.DNS.BootstrapDNS,
|
||||||
|
UpstreamServers: conf.DNS.UpstreamDNS,
|
||||||
|
UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration,
|
||||||
|
}
|
||||||
|
err = m.updateDNS(ctx, dnsConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("assembling dnssvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
webSvcConf := &websvc.Config{
|
||||||
|
ConfigManager: m,
|
||||||
|
// TODO(a.garipov): Fill from config file.
|
||||||
|
TLS: nil,
|
||||||
|
Start: start,
|
||||||
|
Addresses: conf.HTTP.Addresses,
|
||||||
|
SecureAddresses: conf.HTTP.SecureAddresses,
|
||||||
|
Timeout: conf.HTTP.Timeout.Duration,
|
||||||
|
ForceHTTPS: conf.HTTP.ForceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.updateWeb(ctx, webSvcConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("assembling websvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS returns the current DNS service. It is safe for concurrent use.
|
||||||
|
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
m.updMu.RLock()
|
||||||
|
defer m.updMu.RUnlock()
|
||||||
|
|
||||||
|
return m.dns
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager. The
|
||||||
|
// fields of c must not be modified after calling UpdateDNS.
|
||||||
|
func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
m.updMu.Lock()
|
||||||
|
defer m.updMu.Unlock()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||||
|
// error if something went wrong.
|
||||||
|
|
||||||
|
err = m.updateDNS(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reassembling dnssvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
|
||||||
|
func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
if prev := m.dns; prev != nil {
|
||||||
|
err = prev.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shutting down dns svc: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := dnssvc.New(c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating dns svc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dns = svc
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web returns the current web service. It is safe for concurrent use.
|
||||||
|
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
m.updMu.RLock()
|
||||||
|
defer m.updMu.RUnlock()
|
||||||
|
|
||||||
|
return m.web
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager. The
|
||||||
|
// fields of c must not be modified after calling UpdateWeb.
|
||||||
|
func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
m.updMu.Lock()
|
||||||
|
defer m.updMu.Unlock()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||||
|
// error if something went wrong.
|
||||||
|
|
||||||
|
err = m.updateWeb(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reassembling websvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateWeb recreates the web service. m.upd is expected to be locked.
|
||||||
|
func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
if prev := m.web; prev != nil {
|
||||||
|
err = prev.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shutting down web svc: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.web = websvc.New(c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -28,7 +29,7 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||||
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
||||||
var numStarted uint64
|
var numStarted uint64
|
||||||
confMgr := newConfigManager()
|
confMgr := newConfigManager()
|
||||||
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) {
|
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||||
OnStart: func() (err error) {
|
OnStart: func() (err error) {
|
||||||
atomic.AddUint64(&numStarted, 1)
|
atomic.AddUint64(&numStarted, 1)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
|
||||||
// TODO(a.garipov): Consider better ways to do this.
|
// TODO(a.garipov): Consider better ways to do this.
|
||||||
const maxUpdDur = 10 * time.Second
|
const maxUpdDur = 10 * time.Second
|
||||||
updStart := time.Now()
|
updStart := time.Now()
|
||||||
var newSvc ServiceWithConfig[*Config]
|
var newSvc agh.ServiceWithConfig[*Config]
|
||||||
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||||
if time.Since(updStart) >= maxUpdDur {
|
if time.Since(updStart) >= maxUpdDur {
|
||||||
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -24,7 +25,7 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
confMgr := newConfigManager()
|
confMgr := newConfigManager()
|
||||||
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) {
|
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
return websvc.New(&websvc.Config{
|
return websvc.New(&websvc.Config{
|
||||||
TLS: &tls.Config{
|
TLS: &tls.Config{
|
||||||
Certificates: []tls.Certificate{{}},
|
Certificates: []tls.Certificate{{}},
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -33,7 +34,7 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
confMgr := newConfigManager()
|
confMgr := newConfigManager()
|
||||||
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) {
|
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
c, err := dnssvc.New(&dnssvc.Config{
|
c, err := dnssvc.New(&dnssvc.Config{
|
||||||
Addresses: wantDNS.Addresses,
|
Addresses: wantDNS.Addresses,
|
||||||
UpstreamServers: wantDNS.UpstreamServers,
|
UpstreamServers: wantDNS.UpstreamServers,
|
||||||
|
@ -45,7 +46,7 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) {
|
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
return websvc.New(&websvc.Config{
|
return websvc.New(&websvc.Config{
|
||||||
TLS: &tls.Config{
|
TLS: &tls.Config{
|
||||||
Certificates: []tls.Certificate{{}},
|
Certificates: []tls.Certificate{{}},
|
||||||
|
|
|
@ -24,21 +24,10 @@ import (
|
||||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
httptreemux "github.com/dimfeld/httptreemux/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceWithConfig is an extension of the [agh.Service] interface for services
|
|
||||||
// that can return their configuration.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
|
||||||
// how to make it testable in a better way.
|
|
||||||
type ServiceWithConfig[ConfigType any] interface {
|
|
||||||
agh.Service
|
|
||||||
|
|
||||||
Config() (c ConfigType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigManager is the configuration manager interface.
|
// ConfigManager is the configuration manager interface.
|
||||||
type ConfigManager interface {
|
type ConfigManager interface {
|
||||||
DNS() (svc ServiceWithConfig[*dnssvc.Config])
|
DNS() (svc agh.ServiceWithConfig[*dnssvc.Config])
|
||||||
Web() (svc ServiceWithConfig[*Config])
|
Web() (svc agh.ServiceWithConfig[*Config])
|
||||||
|
|
||||||
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
|
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
|
||||||
UpdateWeb(ctx context.Context, c *Config) (err error)
|
UpdateWeb(ctx context.Context, c *Config) (err error)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
@ -34,20 +35,20 @@ var _ websvc.ConfigManager = (*configManager)(nil)
|
||||||
|
|
||||||
// configManager is a [websvc.ConfigManager] for tests.
|
// configManager is a [websvc.ConfigManager] for tests.
|
||||||
type configManager struct {
|
type configManager struct {
|
||||||
onDNS func() (svc websvc.ServiceWithConfig[*dnssvc.Config])
|
onDNS func() (svc agh.ServiceWithConfig[*dnssvc.Config])
|
||||||
onWeb func() (svc websvc.ServiceWithConfig[*websvc.Config])
|
onWeb func() (svc agh.ServiceWithConfig[*websvc.Config])
|
||||||
|
|
||||||
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
|
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
|
||||||
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
|
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS implements the [websvc.ConfigManager] interface for *configManager.
|
// DNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
func (m *configManager) DNS() (svc websvc.ServiceWithConfig[*dnssvc.Config]) {
|
func (m *configManager) DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
return m.onDNS()
|
return m.onDNS()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web implements the [websvc.ConfigManager] interface for *configManager.
|
// Web implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
func (m *configManager) Web() (svc websvc.ServiceWithConfig[*websvc.Config]) {
|
func (m *configManager) Web() (svc agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
return m.onWeb()
|
return m.onWeb()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,8 +65,8 @@ func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err er
|
||||||
// newConfigManager returns a *configManager all methods of which panic.
|
// newConfigManager returns a *configManager all methods of which panic.
|
||||||
func newConfigManager() (m *configManager) {
|
func newConfigManager() (m *configManager) {
|
||||||
return &configManager{
|
return &configManager{
|
||||||
onDNS: func() (svc websvc.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
|
onDNS: func() (svc agh.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
|
||||||
onWeb: func() (svc websvc.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
|
onWeb: func() (svc agh.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
|
||||||
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
|
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
},
|
},
|
||||||
|
|
|
@ -124,11 +124,11 @@ GO111MODULE='on'
|
||||||
export CGO_ENABLED GO111MODULE
|
export CGO_ENABLED GO111MODULE
|
||||||
|
|
||||||
# Build the new binary if requested.
|
# Build the new binary if requested.
|
||||||
if [ "${V1API:-0}" -eq '0' ]
|
if [ "${NEXTAPI:-0}" -eq '0' ]
|
||||||
then
|
then
|
||||||
tags_flags='--tags='
|
tags_flags='--tags='
|
||||||
else
|
else
|
||||||
tags_flags='--tags=v1'
|
tags_flags='--tags=next'
|
||||||
fi
|
fi
|
||||||
readonly tags_flags
|
readonly tags_flags
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue