Pull request 1897: nextapi-write-conf

Squashed commit of the following:

commit 72f25ffe73d6b8216b01e590fba66fb5f6944113
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Jun 28 21:29:04 2023 +0300

    next: add conf writing, validation
This commit is contained in:
Ainar Garipov
2023-06-29 14:34:06 +03:00
parent 2069eddf98
commit d4a4bda645
10 changed files with 232 additions and 37 deletions

View File

@@ -1,5 +1,7 @@
// Package configmgr defines the AdGuard Home on-disk configuration entities and
// configuration manager.
//
// TODO(a.garipov): Add tests.
package configmgr
import (
@@ -14,6 +16,10 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/google/renameio/maybe"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
)
@@ -39,17 +45,58 @@ type Manager struct {
fileName string
}
// Validate returns an error if the configuration file with the given name does
// not exist or is invalid.
func Validate(fileName string) (err error) {
conf, err := read(fileName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
// Don't wrap the error, because it's informative enough as is.
return conf.validate()
}
// 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(
ctx context.Context,
fileName string,
frontend fs.FS,
start time.Time,
) (m *Manager, err error) {
conf, err := read(fileName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
err = conf.validate()
if err != nil {
return nil, fmt.Errorf("validating config: %w", err)
}
m = &Manager{
updMu: &sync.RWMutex{},
current: conf,
fileName: fileName,
}
err = m.assemble(ctx, conf, frontend, start)
if err != nil {
return nil, fmt.Errorf("creating config manager: %w", err)
}
return m, nil
}
// read reads and decodes configuration from the provided filename.
func read(fileName string) (conf *config, err error) {
defer func() { err = errors.Annotate(err, "reading config: %w") }()
conf := &config{}
conf = &config{}
f, err := os.Open(fileName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
@@ -63,27 +110,7 @@ func New(
return nil, err
}
// 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, frontend, start)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return m, nil
return conf, nil
}
// assemble creates all services and puts them into the corresponding fields.
@@ -128,6 +155,23 @@ func (m *Manager) assemble(
return nil
}
// write writes the current configuration to disk.
func (m *Manager) write() (err error) {
b, err := yaml.Marshal(m.current)
if err != nil {
return fmt.Errorf("encoding: %w", err)
}
err = maybe.WriteFile(m.fileName, b, 0o755)
if err != nil {
return fmt.Errorf("writing: %w", err)
}
log.Info("configmgr: written to %q", m.fileName)
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()
@@ -150,7 +194,9 @@ func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
return fmt.Errorf("reassembling dnssvc: %w", err)
}
return nil
m.updateCurrentDNS(c)
return m.write()
}
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
@@ -172,6 +218,17 @@ func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
return nil
}
// updateCurrentDNS updates the DNS configuration in the current config.
func (m *Manager) updateCurrentDNS(c *dnssvc.Config) {
m.current.DNS.Addresses = slices.Clone(c.Addresses)
m.current.DNS.BootstrapDNS = slices.Clone(c.BootstrapServers)
m.current.DNS.UpstreamDNS = slices.Clone(c.UpstreamServers)
m.current.DNS.DNS64Prefixes = slices.Clone(c.DNS64Prefixes)
m.current.DNS.UpstreamTimeout = timeutil.Duration{Duration: c.UpstreamTimeout}
m.current.DNS.BootstrapPreferIPv6 = c.BootstrapPreferIPv6
m.current.DNS.UseDNS64 = c.UseDNS64
}
// Web returns the current web service. It is safe for concurrent use.
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
m.updMu.RLock()
@@ -194,7 +251,9 @@ func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
return fmt.Errorf("reassembling websvc: %w", err)
}
return nil
m.updateCurrentWeb(c)
return m.write()
}
// updateWeb recreates the web service. m.upd is expected to be locked.
@@ -213,3 +272,11 @@ func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
return nil
}
// updateCurrentWeb updates the web configuration in the current config.
func (m *Manager) updateCurrentWeb(c *websvc.Config) {
m.current.HTTP.Addresses = slices.Clone(c.Addresses)
m.current.HTTP.SecureAddresses = slices.Clone(c.SecureAddresses)
m.current.HTTP.Timeout = timeutil.Duration{Duration: c.Timeout}
m.current.HTTP.ForceHTTPS = c.ForceHTTPS
}