AdGuardHome/internal/next/configmgr/configmgr.go
Ainar Garipov 681c604c22 Pull request 1870: nextapi-frontend-fs
Merge in DNS/adguard-home from nextapi-frontend-fs to master

Squashed commit of the following:

commit 3ed959f21939cf5590c27426af46906cbffed502
Merge: e60bbdd04 9fda7bfd3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jun 13 13:37:00 2023 +0300

    Merge branch 'master' into nextapi-frontend-fs

commit e60bbdd04ce841c1aaaa198cc9dc85ae14799ffa
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jun 9 14:53:09 2023 +0300

    next: support frontend fs
2023-06-13 13:41:13 +03:00

219 lines
5.5 KiB
Go

// Package configmgr defines the AdGuard Home on-disk configuration entities and
// configuration manager.
package configmgr
import (
"context"
"fmt"
"io/fs"
"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,
frontend fs.FS,
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, frontend, 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,
frontend fs.FS,
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,
Frontend: frontend,
// 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, err = websvc.New(c)
if err != nil {
return fmt.Errorf("creating web svc: %w", err)
}
return nil
}