chore(config): rework entire global settings reading

- Use qdm12/gosettings
- No functional changes (at least intended)
- Remove qdm12/golibs dependency

chore(params): remove dependency on golibs
This commit is contained in:
Quentin McGaw
2023-06-11 00:18:00 +00:00
parent 05b2535fee
commit 4b51c8b35b
47 changed files with 1024 additions and 738 deletions

View File

@@ -17,7 +17,8 @@ import (
_ "github.com/breml/rootcerts"
"github.com/containrrr/shoutrrr"
"github.com/qdm12/ddns-updater/internal/backup"
"github.com/qdm12/ddns-updater/internal/config"
globsettings "github.com/qdm12/ddns-updater/internal/config/settings"
"github.com/qdm12/ddns-updater/internal/config/sources/env"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/health"
"github.com/qdm12/ddns-updater/internal/models"
@@ -28,7 +29,6 @@ import (
"github.com/qdm12/ddns-updater/internal/server"
"github.com/qdm12/ddns-updater/internal/update"
"github.com/qdm12/ddns-updater/pkg/publicip"
"github.com/qdm12/golibs/params"
"github.com/qdm12/goshutdown"
"github.com/qdm12/gosplash"
"github.com/qdm12/log"
@@ -47,8 +47,8 @@ func main() {
Commit: commit,
BuildDate: buildDate,
}
env := params.New()
logger := log.New()
env := env.New(logger)
ctx := context.Background()
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
@@ -95,22 +95,25 @@ var (
errShoutrrrSetup = errors.New("failed setting up Shoutrrr")
)
func _main(ctx context.Context, env params.Interface, args []string, logger log.LoggerInterface,
func _main(ctx context.Context, settingsSource SettingsSource, args []string, logger log.LoggerInterface,
buildInfo models.BuildInformation, timeNow func() time.Time) (err error) {
if health.IsClientMode(args) {
// Running the program in a separate instance through the Docker
// built-in healthcheck, in an ephemeral fashion to query the
// long running instance of the program about its status
client := health.NewClient()
var healthConfig config.Health
_, err := healthConfig.Get(env)
healthSettings := settingsSource.ReadHealth()
healthSettings.SetDefaults()
err = healthSettings.Validate()
if err != nil {
return err
return fmt.Errorf("health settings: %w", err)
}
return client.Query(ctx, healthConfig.Port)
client := health.NewClient()
return client.Query(ctx, *healthSettings.ServerAddress)
}
announcementExp, err := time.Parse(time.RFC3339, "2021-07-22T00:00:00Z")
announcementExp, err := time.Parse(time.RFC3339, "2023-06-30T00:00:00Z")
if err != nil {
return err
}
@@ -121,7 +124,7 @@ func _main(ctx context.Context, env params.Interface, args []string, logger log.
Version: buildInfo.Version,
Commit: buildInfo.Commit,
BuildDate: buildInfo.BuildDate,
Announcement: "",
Announcement: "Environment variables parsing was changed on 12 June, please report any issue you might have",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
@@ -131,18 +134,19 @@ func _main(ctx context.Context, env params.Interface, args []string, logger log.
fmt.Println(line)
}
var config config.Config
warnings, err := config.Get(env)
for _, warning := range warnings {
logger.Warn(warning)
}
config, err := settingsSource.Read()
if err != nil {
return err
return fmt.Errorf("reading settings: %w", err)
}
config.SetDefaults()
err = config.Validate()
if err != nil {
return fmt.Errorf("settings validation: %w", err)
}
// Setup logger
options := []log.Option{log.SetLevel(config.Logger.Level)}
if config.Logger.Caller {
options := []log.Option{log.SetLevel(*config.Logger.Level)}
if *config.Logger.Caller {
options = append(options, log.SetCallerFile(true), log.SetCallerLine(true))
}
logger.Patch(options...)
@@ -161,14 +165,15 @@ func _main(ctx context.Context, env params.Interface, args []string, logger log.
}
}
persistentDB, err := persistence.NewDatabase(config.Paths.DataDir)
persistentDB, err := persistence.NewDatabase(*config.Paths.DataDir)
if err != nil {
notify(err.Error())
return err
}
jsonReader := jsonparams.NewReader(logger)
settings, warnings, err := jsonReader.JSONSettings(config.Paths.JSON)
jsonFilepath := filepath.Join(*config.Paths.DataDir, "config.json")
settings, warnings, err := jsonReader.JSONSettings(jsonFilepath)
for _, w := range warnings {
logger.Warn(w)
notify(w)
@@ -216,9 +221,17 @@ func _main(ctx context.Context, env params.Interface, args []string, logger log.
}
}()
config.PubIP.HTTPSettings.Client = client
httpSettings := publicip.HTTPSettings{
Enabled: *config.PubIP.HTTPEnabled,
Client: client,
Options: config.PubIP.ToHTTPOptions(),
}
dnsSettings := publicip.DNSSettings{
Enabled: *config.PubIP.HTTPEnabled,
Options: config.PubIP.ToDNSPOptions(),
}
ipGetter, err := publicip.NewFetcher(config.PubIP.DNSSettings, config.PubIP.HTTPSettings)
ipGetter, err := publicip.NewFetcher(dnsSettings, httpSettings)
if err != nil {
return err
}
@@ -241,22 +254,22 @@ func _main(ctx context.Context, env params.Interface, args []string, logger log.
isHealthy := health.MakeIsHealthy(db, resolver)
healthLogger := logger.New(log.SetComponent("healthcheck server"))
healthServer := health.NewServer(config.Health.ServerAddress,
healthServer := health.NewServer(*config.Health.ServerAddress,
healthLogger, isHealthy)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler("health server")
go healthServer.Run(healthServerCtx, healthServerDone)
address := ":" + strconv.Itoa(int(config.Server.Port))
address := ":" + fmt.Sprint(*config.Server.Port)
serverLogger := logger.New(log.SetComponent("http server"))
server := server.New(ctx, address, config.Server.RootURL, db, serverLogger, runner)
server := server.New(ctx, address, *config.Server.RootURL, db, serverLogger, runner)
serverHandler, serverCtx, serverDone := goshutdown.NewGoRoutineHandler("server")
go server.Run(serverCtx, serverDone)
notify("Launched with " + strconv.Itoa(len(records)) + " records to watch")
backupHandler, backupCtx, backupDone := goshutdown.NewGoRoutineHandler("backup")
backupLogger := logger.New(log.SetComponent("backup"))
go backupRunLoop(backupCtx, backupDone, config.Backup.Period, config.Paths.DataDir, config.Backup.Directory,
backupLogger, timeNow)
go backupRunLoop(backupCtx, backupDone, *config.Backup.Period, *config.Paths.DataDir,
*config.Backup.Directory, backupLogger, timeNow)
shutdownGroup := goshutdown.NewGroupHandler("")
shutdownGroup.Add(runnerHandler, healthServerHandler, serverHandler, backupHandler)
@@ -307,3 +320,8 @@ func backupRunLoop(ctx context.Context, done chan<- struct{}, backupPeriod time.
}
}
}
type SettingsSource interface {
Read() (settings globsettings.Settings, err error)
ReadHealth() (settings globsettings.Health)
}

3
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/go-chi/chi v4.1.2+incompatible
github.com/golang/mock v1.6.0
github.com/miekg/dns v1.1.54
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
github.com/qdm12/gosettings v0.3.0
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.1.0
github.com/qdm12/log v0.1.0
@@ -30,6 +30,7 @@ require (
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect

12
go.sum
View File

@@ -246,7 +246,6 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
@@ -307,7 +306,6 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -439,7 +437,6 @@ github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -470,7 +467,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -497,7 +493,6 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -524,8 +519,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
github.com/qdm12/gosettings v0.3.0 h1:YutcgQzVaOB3LuLj+Smtoy90JOH/5B5p2IH3BvV3ra4=
github.com/qdm12/gosettings v0.3.0/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
@@ -604,7 +599,6 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -619,6 +613,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

View File

@@ -1,27 +0,0 @@
package config
import (
"fmt"
"time"
"github.com/qdm12/golibs/params"
)
type Backup struct {
Period time.Duration
Directory string
}
func (b *Backup) get(env params.Interface) (err error) {
b.Period, err = env.Duration("BACKUP_PERIOD", params.Default("0"))
if err != nil {
return fmt.Errorf("%w: for environment variable BACKUP_PERIOD", err)
}
b.Directory, err = env.Path("BACKUP_DIRECTORY", params.Default("./data"))
if err != nil {
return fmt.Errorf("%w: for environment variable BACKUP_DIRECTORY", err)
}
return nil
}

View File

@@ -1,21 +0,0 @@
package config
import (
"fmt"
"time"
"github.com/qdm12/golibs/params"
)
type Client struct {
Timeout time.Duration
}
func (c *Client) get(env params.Interface) (err error) {
c.Timeout, err = env.Duration("HTTP_TIMEOUT", params.Default("10s"))
if err != nil {
return fmt.Errorf("%w: for environment variable HTTP_TIMEOUT", err)
}
return nil
}

View File

@@ -1,86 +0,0 @@
package config
import (
"fmt"
"github.com/qdm12/ddns-updater/internal/resolver"
"github.com/qdm12/golibs/params"
)
type Config struct {
Client Client
Update Update
PubIP PubIP
Resolver resolver.Settings
IPv6 IPv6
Server Server
Health Health
Paths Paths
Backup Backup
Logger Logger
Shoutrrr Shoutrrr
}
func (c *Config) Get(env params.Interface) (warnings []string, err error) {
err = c.Client.get(env)
if err != nil {
return warnings, err
}
warning, err := c.Update.get(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
newWarnings, err := c.PubIP.get(env)
warnings = append(warnings, newWarnings...)
if err != nil {
return warnings, err
}
c.Resolver, err = readResolver()
if err != nil {
return warnings, fmt.Errorf("reading resolver settings: %w", err)
}
err = c.IPv6.get(env)
if err != nil {
return warnings, err
}
warning, err = c.Server.get(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
warning, err = c.Health.Get(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
err = c.Paths.get(env)
if err != nil {
return warnings, err
}
err = c.Backup.get(env)
if err != nil {
return warnings, err
}
c.Logger, err = readLog()
if err != nil {
return warnings, err
}
newWarnings, err = c.Shoutrrr.get(env)
warnings = append(warnings, newWarnings...)
if err != nil {
return warnings, err
}
return warnings, nil
}

View File

@@ -1,32 +0,0 @@
package config
import (
"fmt"
"net"
"strconv"
"github.com/qdm12/golibs/params"
)
type Health struct {
ServerAddress string
Port uint16 // obtained from ServerAddress
}
func (h *Health) Get(env params.Interface) (warning string, err error) {
h.ServerAddress, warning, err = env.ListeningAddress(
"HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999"))
if err != nil {
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
}
_, portStr, err := net.SplitHostPort(h.ServerAddress)
if err != nil {
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
}
port, err := strconv.Atoi(portStr)
if err != nil {
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
}
h.Port = uint16(port)
return warning, nil
}

View File

@@ -1,73 +0,0 @@
package config
import (
"errors"
"fmt"
"os"
"strings"
"github.com/qdm12/log"
)
type Logger struct {
Caller bool
Level log.Level
}
var (
ErrLogCallerNotValid = errors.New("LOG_CALLER value is not valid")
)
func readLog() (settings Logger, err error) {
callerString := os.Getenv("LOG_CALLER")
switch callerString {
case "":
case "hidden":
case "short":
settings.Caller = true
default:
return settings, fmt.Errorf("%w: "+
`%q must be one of "", "hidden" or "short"`,
ErrLogCallerNotValid, callerString)
}
settings.Level, err = readLogLevel()
if err != nil {
return settings, err
}
return settings, nil
}
func readLogLevel() (level log.Level, err error) {
s := os.Getenv("LOG_LEVEL")
if s == "" {
return log.LevelInfo, nil
}
level, err = parseLogLevel(s)
if err != nil {
return level, fmt.Errorf("environment variable LOG_LEVEL: %w", err)
}
return level, nil
}
var ErrLogLevelUnknown = errors.New("log level is unknown")
func parseLogLevel(s string) (level log.Level, err error) {
switch strings.ToLower(s) {
case "debug":
return log.LevelDebug, nil
case "info":
return log.LevelInfo, nil
case "warning":
return log.LevelWarn, nil
case "error":
return log.LevelError, nil
default:
return level, fmt.Errorf(
"%w: %q is not valid and can be one of debug, info, warning or error",
ErrLogLevelUnknown, s)
}
}

View File

@@ -1,23 +0,0 @@
package config
import (
"fmt"
"path/filepath"
"github.com/qdm12/golibs/params"
)
type Paths struct {
DataDir string
JSON string // obtained from DataDir
}
func (p *Paths) get(env params.Interface) (err error) {
p.DataDir, err = env.Path("DATADIR", params.Default("./data"))
if err != nil {
return fmt.Errorf("%w: for environment variable DATADIR", err)
}
p.JSON = filepath.Join(p.DataDir, "config.json")
return nil
}

View File

@@ -1,197 +0,0 @@
package config
import (
"errors"
"fmt"
"net/url"
"strings"
"github.com/qdm12/ddns-updater/pkg/publicip"
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
"github.com/qdm12/ddns-updater/pkg/publicip/http"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/golibs/params"
)
const all = "all"
type PubIP struct {
HTTPSettings publicip.HTTPSettings
DNSSettings publicip.DNSSettings
}
func (p *PubIP) get(env params.Interface) (warnings []string, err error) {
err = p.getFetchers(env)
if err != nil {
return nil, err
}
httpIPProviders, warning, err := p.getIPHTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
httpIP4Providers, warning, err := p.getIPv4HTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
httpIP6Providers, warning, err := p.getIPv6HTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
p.HTTPSettings.Options = []http.Option{
http.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
http.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
http.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
}
dnsIPProviders, err := p.getDNSProviders(env)
if err != nil {
return warnings, err
}
dnsTimeout, err := env.Duration("PUBLICIP_DNS_TIMEOUT", params.Default("3s"))
if err != nil {
return warnings, err
}
p.DNSSettings.Options = []dns.Option{
dns.SetTimeout(dnsTimeout),
dns.SetProviders(dnsIPProviders[0], dnsIPProviders[1:]...),
}
return warnings, nil
}
var ErrInvalidFetcher = errors.New("invalid fetcher specified")
func (p *PubIP) getFetchers(env params.Interface) (err error) {
s, err := env.Get("PUBLICIP_FETCHERS", params.Default(all))
if err != nil {
return fmt.Errorf("%w: for environment variable PUBLICIP_FETCHERS", err)
}
fields := strings.Split(s, ",")
for i, field := range fields {
switch strings.ToLower(field) {
case all:
p.HTTPSettings.Enabled = true
p.DNSSettings.Enabled = true
case "http":
p.HTTPSettings.Enabled = true
case "dns":
p.DNSSettings.Enabled = true
default:
err = fmt.Errorf(
"%w: %q at position %d of %d",
ErrInvalidFetcher, field, i+1, len(fields))
}
}
return err
}
// getDNSProviders obtains the DNS providers to obtain your public IPv4 and/or IPv6 address.
func (p *PubIP) getDNSProviders(env params.Interface) (providers []dns.Provider, err error) {
s, err := env.Get("PUBLICIP_DNS_PROVIDERS", params.Default(all))
if err != nil {
return nil, fmt.Errorf("%w: for environment variable PUBLICIP_DNS_PROVIDERS", err)
}
availableProviders := dns.ListProviders()
fields := strings.Split(s, ",")
providers = make([]dns.Provider, len(fields))
for i, field := range fields {
if field == all {
return availableProviders, nil
}
providers[i] = dns.Provider(field)
err = dns.ValidateProvider(providers[i])
if err != nil {
return nil, err
}
}
return providers, nil
}
// getHTTPProviders obtains the HTTP providers to obtain your public IPv4 or IPv6 address.
func (p *PubIP) getIPHTTPProviders(env params.Interface) (
providers []http.Provider, warning string, err error) {
return httpIPMethod(env, "PUBLICIP_HTTP_PROVIDERS", "IP_METHOD", ipversion.IP4or6)
}
// getIPv4HTTPProviders obtains the HTTP providers to obtain your public IPv4 address.
func (p *PubIP) getIPv4HTTPProviders(env params.Interface) (
providers []http.Provider, warning string, err error) {
return httpIPMethod(env, "PUBLICIPV4_HTTP_PROVIDERS", "IPV4_METHOD", ipversion.IP4)
}
// getIPv6HTTPProviders obtains the HTTP providers to obtain your public IPv6 address.
func (p *PubIP) getIPv6HTTPProviders(env params.Interface) (
providers []http.Provider, warning string, err error) {
return httpIPMethod(env, "PUBLICIPV6_HTTP_PROVIDERS", "IPV6_METHOD", ipversion.IP6)
}
var (
ErrInvalidPublicIPHTTPProvider = errors.New("invalid public IP HTTP provider")
)
func httpIPMethod(env params.Interface, envKey, retroKey string, version ipversion.IPVersion) (
providers []http.Provider, warning string, err error) {
retroKeyOption := params.RetroKeys([]string{retroKey}, func(oldKey, newKey string) {
warning = "You are using an old environment variable " + oldKey +
" please change it to " + newKey
})
s, err := env.Get(envKey, params.Default("cycle"), retroKeyOption)
if err != nil {
return nil, warning, fmt.Errorf("%w: for environment variable %s", err, envKey)
}
availableProviders := http.ListProvidersForVersion(version)
choices := make(map[http.Provider]struct{}, len(availableProviders))
for _, provider := range availableProviders {
choices[provider] = struct{}{}
}
fields := strings.Split(s, ",")
for _, field := range fields {
// Retro-compatibility.
switch field {
case "ipify6":
field = "ipify"
case "noip4", "noip6", "noip8245_4", "noip8245_6":
field = "noip"
case "cycle":
field = all
}
if field == all {
return availableProviders, warning, nil
}
// Custom URL check
url, err := url.Parse(field)
if err == nil && url != nil && url.Scheme == "https" {
providers = append(providers, http.CustomProvider(url))
continue
}
provider := http.Provider(field)
if _, ok := choices[provider]; !ok {
return nil, warning, fmt.Errorf("%w: %s", ErrInvalidPublicIPHTTPProvider, provider)
}
providers = append(providers, provider)
}
if len(providers) == 0 {
return nil, warning, fmt.Errorf("%w: for IP version %s", ErrInvalidPublicIPHTTPProvider, version)
}
return providers, warning, nil
}

View File

@@ -1,27 +0,0 @@
package config
import (
"fmt"
"os"
"time"
"github.com/qdm12/ddns-updater/internal/resolver"
)
func readResolver() (settings resolver.Settings, err error) {
address := os.Getenv("RESOLVER_ADDRESS")
if address != "" {
settings.Address = &address
}
timeoutString := os.Getenv("RESOLVER_TIMEOUT")
if timeoutString != "" {
timeout, err := time.ParseDuration(timeoutString)
if err != nil {
return settings, fmt.Errorf("environment variable RESOLVER_TIMEOUT: %w", err)
}
settings.Timeout = timeout
}
return settings, nil
}

View File

@@ -1,26 +0,0 @@
package config
import (
"fmt"
"github.com/qdm12/golibs/params"
)
type Server struct {
Port uint16
RootURL string
}
func (s *Server) get(env params.Interface) (warning string, err error) {
s.RootURL, err = env.RootURL("ROOT_URL")
if err != nil {
return "", fmt.Errorf("%w: for environment variable ROOT_URL", err)
}
s.Port, warning, err = env.ListeningPort("LISTENING_PORT", params.Default("8000"))
if err != nil {
return "", fmt.Errorf("%w: for environment variable LISTENING_PORT", err)
}
return warning, err
}

View File

@@ -0,0 +1,27 @@
package settings
import (
"time"
"github.com/qdm12/gosettings"
)
type Backup struct {
Period *time.Duration
Directory *string
}
func (b *Backup) setDefaults() {
b.Period = gosettings.DefaultPointer(b.Period, 0)
b.Directory = gosettings.DefaultPointer(b.Directory, "./data")
}
func (b Backup) mergeWith(other Backup) (merged Backup) {
merged.Period = gosettings.MergeWithPointer(b.Period, other.Period)
merged.Directory = gosettings.MergeWithPointer(b.Directory, other.Directory)
return merged
}
func (b Backup) Validate() (err error) {
return nil
}

View File

@@ -0,0 +1,25 @@
package settings
import (
"time"
"github.com/qdm12/gosettings"
)
type Client struct {
Timeout time.Duration
}
func (c *Client) setDefaults() {
const defaultTimeout = 10 * time.Second
c.Timeout = gosettings.DefaultNumber(c.Timeout, defaultTimeout)
}
func (c Client) mergeWith(other Client) (merged Client) {
merged.Timeout = gosettings.MergeWithNumber(c.Timeout, other.Timeout)
return merged
}
func (c Client) Validate() (err error) {
return nil
}

View File

@@ -0,0 +1,31 @@
package settings
import (
"fmt"
"os"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate"
)
type Health struct {
ServerAddress *string
}
func (h *Health) SetDefaults() {
h.ServerAddress = gosettings.DefaultPointer(h.ServerAddress, "127.0.0.1:9999")
}
func (h Health) mergeWith(other Health) (merged Health) {
merged.ServerAddress = gosettings.MergeWithPointer(h.ServerAddress, other.ServerAddress)
return merged
}
func (h Health) Validate() (err error) {
err = validate.ListeningAddress(*h.ServerAddress, os.Getuid())
if err != nil {
return fmt.Errorf("server listening address: %w", err)
}
return nil
}

View File

@@ -0,0 +1,3 @@
package settings
const all = "all"

View File

@@ -0,0 +1,25 @@
package settings
import (
"net"
"github.com/qdm12/gosettings"
)
type IPv6 struct {
Mask net.IPMask
}
func (i *IPv6) setDefaults() {
const ipv6Bits = 8 * net.IPv6len
i.Mask = net.CIDRMask(ipv6Bits, ipv6Bits)
}
func (i IPv6) mergeWith(other IPv6) (merged IPv6) {
merged.Mask = gosettings.MergeWithSlice(i.Mask, other.Mask)
return merged
}
func (i IPv6) Validate() (err error) {
return nil
}

View File

@@ -0,0 +1,26 @@
package settings
import (
"github.com/qdm12/gosettings"
"github.com/qdm12/log"
)
type Logger struct {
Caller *bool
Level *log.Level
}
func (l *Logger) setDefaults() {
l.Caller = gosettings.DefaultPointer(l.Caller, false)
l.Level = gosettings.DefaultPointer(l.Level, log.LevelInfo)
}
func (l Logger) mergeWith(other Logger) (merged Logger) {
merged.Caller = gosettings.MergeWithPointer(l.Caller, other.Caller)
merged.Level = gosettings.MergeWithPointer(l.Level, other.Level)
return merged
}
func (l Logger) Validate() (err error) {
return nil
}

View File

@@ -0,0 +1,22 @@
package settings
import (
"github.com/qdm12/gosettings"
)
type Paths struct {
DataDir *string
}
func (p *Paths) setDefaults() {
p.DataDir = gosettings.DefaultPointer(p.DataDir, "./data")
}
func (p Paths) mergeWith(other Paths) (merged Paths) {
merged.DataDir = gosettings.MergeWithPointer(p.DataDir, other.DataDir)
return merged
}
func (p Paths) Validate() (err error) {
return nil
}

View File

@@ -0,0 +1,193 @@
package settings
import (
"errors"
"fmt"
"net/url"
"time"
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
"github.com/qdm12/ddns-updater/pkg/publicip/http"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate"
)
type PubIP struct {
HTTPEnabled *bool
HTTPIPProviders []string
HTTPIPv4Providers []string
HTTPIPv6Providers []string
DNSEnabled *bool
DNSProviders []string
DNSTimeout time.Duration
}
func (p *PubIP) setDefaults() {
p.HTTPEnabled = gosettings.DefaultPointer(p.HTTPEnabled, true)
p.HTTPIPProviders = gosettings.DefaultSlice(p.HTTPIPProviders, []string{all})
p.HTTPIPv4Providers = gosettings.DefaultSlice(p.HTTPIPv4Providers, []string{all})
p.HTTPIPv6Providers = gosettings.DefaultSlice(p.HTTPIPv6Providers, []string{all})
p.DNSEnabled = gosettings.DefaultPointer(p.DNSEnabled, true)
p.DNSProviders = gosettings.DefaultSlice(p.DNSProviders, []string{all})
const defaultDNSTimeout = 3 * time.Second
p.DNSTimeout = gosettings.DefaultNumber(p.DNSTimeout, defaultDNSTimeout)
}
func (p PubIP) mergeWith(other PubIP) (merged PubIP) {
merged.HTTPEnabled = gosettings.MergeWithPointer(p.HTTPEnabled, other.HTTPEnabled)
merged.HTTPIPProviders = gosettings.MergeWithSlice(p.HTTPIPProviders, other.HTTPIPProviders)
merged.HTTPIPv4Providers = gosettings.MergeWithSlice(p.HTTPIPv4Providers, other.HTTPIPv4Providers)
merged.HTTPIPv6Providers = gosettings.MergeWithSlice(p.HTTPIPv6Providers, other.HTTPIPv6Providers)
merged.DNSEnabled = gosettings.MergeWithPointer(p.DNSEnabled, other.DNSEnabled)
merged.DNSProviders = gosettings.MergeWithSlice(p.DNSProviders, other.DNSProviders)
merged.DNSTimeout = gosettings.MergeWithNumber(p.DNSTimeout, other.DNSTimeout)
return merged
}
func (p PubIP) Validate() (err error) {
err = p.validateHTTPIPProviders()
if err != nil {
return fmt.Errorf("HTTP IP providers: %w", err)
}
err = p.validateHTTPIPv4Providers()
if err != nil {
return fmt.Errorf("HTTP IPv4 providers: %w", err)
}
err = p.validateHTTPIPv6Providers()
if err != nil {
return fmt.Errorf("HTTP IPv6 providers: %w", err)
}
err = p.validateDNSProviders()
if err != nil {
return fmt.Errorf("DNS providers: %w", err)
}
return nil
}
// ToHTTPOptions assumes the settings have been validated.
func (p *PubIP) ToHTTPOptions() (options []http.Option) {
httpIPProviders := stringsToHTTPProviders(p.HTTPIPProviders, ipversion.IP4or6)
httpIPv4Providers := stringsToHTTPProviders(p.HTTPIPv4Providers, ipversion.IP4)
httpIPv6Providers := stringsToHTTPProviders(p.HTTPIPv6Providers, ipversion.IP6)
return []http.Option{
http.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
http.SetProvidersIP4(httpIPv4Providers[0], httpIPv4Providers[1:]...),
http.SetProvidersIP6(httpIPv6Providers[0], httpIPv6Providers[1:]...),
}
}
func stringsToHTTPProviders(providers []string, ipVersion ipversion.IPVersion) (
updatedProviders []http.Provider) {
updatedProvidersSet := make(map[string]struct{}, len(providers))
for _, provider := range providers {
if provider != all {
updatedProvidersSet[provider] = struct{}{}
continue
}
allProviders := http.ListProvidersForVersion(ipVersion)
for _, provider := range allProviders {
updatedProvidersSet[string(provider)] = struct{}{}
}
}
updatedProviders = make([]http.Provider, 0, len(updatedProvidersSet))
for provider := range updatedProvidersSet {
updatedProviders = append(updatedProviders, http.Provider(provider))
}
return updatedProviders
}
// ToDNSPOptions assumes the settings have been validated.
func (p *PubIP) ToDNSPOptions() (options []dns.Option) {
uniqueProviders := make(map[string]struct{}, len(p.DNSProviders))
for _, provider := range p.DNSProviders {
if provider != all {
uniqueProviders[provider] = struct{}{}
}
allProviders := dns.ListProviders()
for _, provider := range allProviders {
uniqueProviders[string(provider)] = struct{}{}
}
}
providers := make([]dns.Provider, 0, len(p.HTTPIPProviders))
for providerString := range uniqueProviders {
providers = append(providers, dns.Provider(providerString))
}
return []dns.Option{
dns.SetTimeout(p.DNSTimeout),
dns.SetProviders(providers[0], providers[1:]...),
}
}
var (
ErrNoPublicIPDNSProvider = errors.New("no public IP DNS provider specified")
)
func (p *PubIP) validateDNSProviders() (err error) {
if len(p.DNSProviders) == 0 {
return fmt.Errorf("%w", ErrNoPublicIPDNSProvider)
}
availableProviders := dns.ListProviders()
validChoices := make([]string, len(availableProviders)+1)
for i, provider := range availableProviders {
validChoices[i] = string(provider)
}
validChoices[len(validChoices)-1] = all
return validate.AreAllOneOf(p.DNSProviders, validChoices)
}
func (p *PubIP) validateHTTPIPProviders() (err error) {
return validateHTTPIPProviders(p.HTTPIPProviders, ipversion.IP4or6)
}
func (p *PubIP) validateHTTPIPv4Providers() (err error) {
return validateHTTPIPProviders(p.HTTPIPv4Providers, ipversion.IP4)
}
func (p *PubIP) validateHTTPIPv6Providers() (err error) {
return validateHTTPIPProviders(p.HTTPIPv6Providers, ipversion.IP6)
}
var (
ErrNoPublicIPHTTPProvider = errors.New("no public IP HTTP provider specified")
)
func validateHTTPIPProviders(providerStrings []string,
version ipversion.IPVersion) (err error) {
if len(providerStrings) == 0 {
return fmt.Errorf("%w", ErrNoPublicIPHTTPProvider)
}
availableProviders := http.ListProvidersForVersion(version)
choices := make(map[string]struct{}, len(availableProviders)+1)
choices[all] = struct{}{}
for i := range availableProviders {
choices[string(availableProviders[i])] = struct{}{}
}
for _, providerString := range providerStrings {
// Custom URL check
url, err := url.Parse(providerString)
if err == nil && url != nil && url.Scheme == "https" {
continue
}
_, ok := choices[providerString]
if !ok {
return fmt.Errorf("%w: %s", validate.ErrValueNotOneOf, providerString)
}
}
return nil
}

View File

@@ -0,0 +1,38 @@
package settings
import (
"fmt"
"os"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate"
)
type Server struct {
Port *uint16
RootURL *string
}
func (s *Server) setDefaults() {
const defaultPort = 8000
s.Port = gosettings.DefaultPointer(s.Port, defaultPort)
s.RootURL = gosettings.DefaultPointer(s.RootURL, "")
}
func (s Server) mergeWith(other Server) (merged Server) {
merged.Port = gosettings.MergeWithPointer(s.Port, other.Port)
merged.RootURL = gosettings.MergeWithPointer(s.RootURL, other.RootURL)
return merged
}
func (s Server) Validate() (err error) {
listeningAddress := ":" + fmt.Sprint(*s.Port)
err = validate.ListeningAddress(listeningAddress, os.Getuid())
if err != nil {
return fmt.Errorf("listening address: %w", err)
}
// TODO validate RootURL
return nil
}

View File

@@ -0,0 +1,78 @@
package settings
import (
"fmt"
"github.com/qdm12/ddns-updater/internal/resolver"
)
type Settings struct {
Client Client
Update Update
PubIP PubIP
Resolver resolver.Settings
IPv6 IPv6
Server Server
Health Health
Paths Paths
Backup Backup
Logger Logger
Shoutrrr Shoutrrr
}
func (s *Settings) SetDefaults() {
s.Client.setDefaults()
s.Update.setDefaults()
s.PubIP.setDefaults()
s.Resolver.SetDefaults()
s.IPv6.setDefaults()
s.Server.setDefaults()
s.Health.SetDefaults()
s.Paths.setDefaults()
s.Backup.setDefaults()
s.Logger.setDefaults()
s.Shoutrrr.setDefaults()
}
func (s Settings) MergeWith(other Settings) (merged Settings) {
merged.Client = s.Client.mergeWith(other.Client)
merged.Update = s.Update.mergeWith(other.Update)
merged.PubIP = s.PubIP.mergeWith(other.PubIP)
merged.Resolver = s.Resolver.MergeWith(other.Resolver)
merged.IPv6 = s.IPv6.mergeWith(other.IPv6)
merged.Server = s.Server.mergeWith(other.Server)
merged.Health = s.Health.mergeWith(other.Health)
merged.Paths = s.Paths.mergeWith(other.Paths)
merged.Backup = s.Backup.mergeWith(other.Backup)
merged.Logger = s.Logger.mergeWith(other.Logger)
merged.Shoutrrr = s.Shoutrrr.mergeWith(other.Shoutrrr)
return merged
}
func (s Settings) Validate() (err error) {
type validator interface {
Validate() (err error)
}
toValidate := map[string]validator{
"client": &s.Client,
"update": &s.Update,
"public ip": &s.PubIP,
"resolver": &s.Resolver,
"ipv6": &s.IPv6,
"server": &s.Server,
"health": &s.Health,
"paths": &s.Paths,
"backup": &s.Backup,
"logger": &s.Logger,
"shoutrrr": &s.Shoutrrr,
}
for name, v := range toValidate {
err = v.Validate()
if err != nil {
return fmt.Errorf("%s settings: %w", name, err)
}
}
return nil
}

View File

@@ -0,0 +1,37 @@
package settings
import (
"fmt"
"github.com/containrrr/shoutrrr"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/qdm12/gosettings"
)
type Shoutrrr struct {
Addresses []string
Params types.Params
}
func (s *Shoutrrr) setDefaults() {
s.Addresses = []string{}
s.Params = types.Params{
"title": "DDNS Updater",
}
}
func (s Shoutrrr) mergeWith(other Shoutrrr) (merged Shoutrrr) {
merged.Addresses = gosettings.MergeWithSlice(s.Addresses, other.Addresses)
if s.Params == nil {
merged.Params = other.Params
}
return merged
}
func (s Shoutrrr) Validate() (err error) {
_, err = shoutrrr.CreateSender(s.Addresses...)
if err != nil {
return fmt.Errorf("shoutrrr addresses: %w", err)
}
return nil
}

View File

@@ -0,0 +1,29 @@
package settings
import (
"time"
"github.com/qdm12/gosettings"
)
type Update struct {
Period time.Duration
Cooldown time.Duration
}
func (u *Update) setDefaults() {
const defaultPeriod = 10 * time.Minute
u.Period = gosettings.DefaultNumber(u.Period, defaultPeriod)
const defaultCooldown = 5 * time.Minute
u.Cooldown = gosettings.DefaultNumber(u.Cooldown, defaultCooldown)
}
func (u Update) mergeWith(other Update) (merged Update) {
merged.Period = gosettings.MergeWithNumber(u.Period, other.Period)
merged.Cooldown = gosettings.MergeWithNumber(u.Cooldown, other.Cooldown)
return merged
}
func (u Update) Validate() (err error) {
return nil
}

View File

@@ -1,71 +0,0 @@
package config
import (
"fmt"
"net/url"
"path"
"strings"
"github.com/containrrr/shoutrrr"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/qdm12/golibs/params"
)
type Shoutrrr struct {
Addresses []string
Params types.Params
}
func (s *Shoutrrr) get(env params.Interface) (warnings []string, err error) {
s.Addresses, err = env.CSV("SHOUTRRR_ADDRESSES", params.CaseSensitiveValue())
if err != nil {
return nil, fmt.Errorf("%w: for environment variable SHOUTRRR_ADDRESSES", err)
}
// Retro-compatibility: GOTIFY_URL and GOTIFY_TOKEN
gotifyURL, err := env.URL("GOTIFY_URL")
if err != nil || gotifyURL != nil {
const warning = "You should use the environment variable SHOUTRRR_ADDRESSES instead of GOTIFY_URL and GOTIFY_TOKEN"
warnings = append(warnings, warning)
}
if err != nil {
return nil, fmt.Errorf("%w: for environment variable GOTIFY_URL", err)
} else if gotifyURL != nil {
gotifyToken, err := env.Get("GOTIFY_TOKEN", params.CaseSensitiveValue(),
params.Compulsory(), params.Unset())
if err != nil {
return warnings, err
}
gotifyShoutrrrAddress := gotifyURLTokenToShoutrrr(gotifyURL, gotifyToken)
s.Addresses = append(s.Addresses, gotifyShoutrrrAddress)
}
_, err = shoutrrr.CreateSender(s.Addresses...)
if err != nil {
return warnings, fmt.Errorf("for environment variable SHOUTRRR_ADDRESSES: %w", err) // validation step
}
str, err := env.Get("SHOUTRRR_PARAMS", params.Default("title=DDNS Updater"), params.CaseSensitiveValue())
if err != nil {
return warnings, fmt.Errorf("%w: for environment variable SHOUTRRR_PARAMS", err)
}
keyValues := strings.Split(str, ",")
s.Params = make(map[string]string, len(keyValues))
for _, keyValue := range keyValues {
fields := strings.Split(keyValue, "=")
key, value := fields[0], fields[1]
s.Params[key] = value
}
return warnings, nil
}
func gotifyURLTokenToShoutrrr(url *url.URL, token string) (address string) {
hostAndPath := path.Join(url.Host, url.Path)
address = "gotify://" + hostAndPath + "/" + token
if url.Scheme == "http" {
address += "?DisableTLS=Yes"
}
return address
}

16
internal/config/sources/env/backup.go vendored Normal file
View File

@@ -0,0 +1,16 @@
package env
import (
"github.com/qdm12/ddns-updater/internal/config/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readBackup() (settings settings.Backup, err error) {
settings.Period, err = s.env.DurationPtr("BACKUP_PERIOD")
if err != nil {
return settings, err
}
settings.Directory = s.env.Get("BACKUP_DIRECTORY", env.ForceLowercase(false))
return settings, nil
}

10
internal/config/sources/env/client.go vendored Normal file
View File

@@ -0,0 +1,10 @@
package env
import (
"github.com/qdm12/ddns-updater/internal/config/settings"
)
func (s *Source) readClient() (settings settings.Client, err error) {
settings.Timeout, err = s.env.Duration("HTTP_TIMEOUT")
return settings, err
}

10
internal/config/sources/env/health.go vendored Normal file
View File

@@ -0,0 +1,10 @@
package env
import (
"github.com/qdm12/ddns-updater/internal/config/settings"
)
func (s *Source) ReadHealth() (settings settings.Health) {
settings.ServerAddress = s.env.Get("HTTP_TIMEOUT")
return settings
}

View File

@@ -0,0 +1,5 @@
package env
func ptrTo[T any](v T) *T {
return &v
}

View File

@@ -0,0 +1,5 @@
package env
type Warner interface {
Warnf(format string, args ...any)
}

View File

@@ -1,4 +1,4 @@
package config
package env
import (
"errors"
@@ -6,24 +6,21 @@ import (
"net"
"strings"
"github.com/qdm12/golibs/params"
"github.com/qdm12/ddns-updater/internal/config/settings"
)
type IPv6 struct {
Mask net.IPMask
}
func (i *IPv6) get(env params.Interface) (err error) {
maskStr, err := env.Get("IPV6_PREFIX", params.Default("/128"))
if err != nil {
return fmt.Errorf("%w: for environment variable IPV6_PREFIX", err)
}
i.Mask, err = ipv6DecimalPrefixToMask(maskStr)
if err != nil {
return fmt.Errorf("%w: for environment variable IPV6_PREFIX", err)
func (s *Source) readIPv6() (settings settings.IPv6, err error) {
maskStr := s.env.String("IPV6_MASK")
if maskStr == "" {
return settings, nil
}
return nil
settings.Mask, err = ipv6DecimalPrefixToMask(maskStr)
if err != nil {
return settings, fmt.Errorf("%w: for environment variable IPV6_PREFIX", err)
}
return settings, nil
}
var ErrParsePrefix = errors.New("cannot parse IP prefix")

View File

@@ -1,4 +1,4 @@
package config
package env
import (
"fmt"

75
internal/config/sources/env/logger.go vendored Normal file
View File

@@ -0,0 +1,75 @@
package env
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/ddns-updater/internal/config/settings"
"github.com/qdm12/gosettings/sources/env"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/log"
)
func (s *Source) readLogger() (settings settings.Logger, err error) {
settings.Caller, err = readCaller(s.env)
if err != nil {
return settings, err
}
settings.Level, err = readLogLevel(s.env)
if err != nil {
return settings, err
}
return settings, nil
}
func readCaller(env env.Env) (caller *bool, err error) {
callerString := env.String("LOG_CALLER")
switch callerString {
case "":
return nil, nil //nolint:nilnil
case "hidden":
return ptrTo(false), nil
case "short":
return ptrTo(true), nil
default:
err = validate.IsOneOf(callerString, "", "hidden", "short")
return nil, fmt.Errorf("environment variable LOG_CALLER: %w", err)
}
}
func readLogLevel(env env.Env) (level *log.Level, err error) {
s := env.String("LOG_LEVEL")
if s == "" {
return nil, nil //nolint:nilnil
}
level = new(log.Level)
*level, err = parseLogLevel(s)
if err != nil {
return nil, fmt.Errorf("environment variable LOG_LEVEL: %w", err)
}
return level, nil
}
var ErrLogLevelUnknown = errors.New("log level is unknown")
func parseLogLevel(s string) (level log.Level, err error) {
switch strings.ToLower(s) {
case "debug":
return log.LevelDebug, nil
case "info":
return log.LevelInfo, nil
case "warning":
return log.LevelWarn, nil
case "error":
return log.LevelError, nil
default:
return level, fmt.Errorf(
"%w: %q is not valid and can be one of debug, info, warning or error",
ErrLogLevelUnknown, s)
}
}

10
internal/config/sources/env/paths.go vendored Normal file
View File

@@ -0,0 +1,10 @@
package env
import (
"github.com/qdm12/ddns-updater/internal/config/settings"
)
func (s *Source) readPaths() (settings settings.Paths) {
settings.DataDir = s.env.Get("DATADIR")
return settings
}

85
internal/config/sources/env/pubip.go vendored Normal file
View File

@@ -0,0 +1,85 @@
package env
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/ddns-updater/internal/config/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readPubIP() (settings settings.PubIP, err error) {
settings.HTTPEnabled, settings.DNSEnabled, err = getFetchers(s.env)
if err != nil {
return settings, err
}
settings.HTTPIPProviders = s.env.CSV("PUBLICIP_HTTP_PROVIDERS",
env.RetroKeys("IP_METHOD"))
settings.HTTPIPv4Providers = s.env.CSV("PUBLICIPV4_HTTP_PROVIDERS",
env.RetroKeys("IPV4_METHOD"))
settings.HTTPIPv6Providers = s.env.CSV("PUBLICIPV6_HTTP_PROVIDERS",
env.RetroKeys("IPV6_METHOD"))
// Retro-compatibility
for i := range settings.HTTPIPProviders {
settings.HTTPIPProviders[i] = handleRetroProvider(settings.HTTPIPProviders[i])
}
for i := range settings.HTTPIPv4Providers {
settings.HTTPIPv4Providers[i] = handleRetroProvider(settings.HTTPIPv4Providers[i])
}
for i := range settings.HTTPIPv6Providers {
settings.HTTPIPv6Providers[i] = handleRetroProvider(settings.HTTPIPv6Providers[i])
}
settings.DNSProviders = s.env.CSV("PUBLICIP_DNS_PROVIDERS")
settings.DNSTimeout, err = s.env.Duration("PUBLICIP_DNS_TIMEOUT")
if err != nil {
return settings, err
}
return settings, nil
}
var ErrInvalidFetcher = errors.New("invalid fetcher specified")
func getFetchers(env env.Env) (http, dns *bool, err error) {
// TODO change to use env.BoolPtr with retro-compatibility
s := env.String("PUBLICIP_FETCHERS")
if s == "" {
return nil, nil, nil
}
fields := strings.Split(s, ",")
for i, field := range fields {
switch strings.ToLower(field) {
case "all":
http = ptrTo(true)
dns = ptrTo(true)
case "http":
http = ptrTo(true)
case "dns":
dns = ptrTo(true)
default:
return nil, nil, fmt.Errorf(
"%w: %q at position %d of %d",
ErrInvalidFetcher, field, i+1, len(fields))
}
}
return http, dns, nil
}
func handleRetroProvider(provider string) (updatedProvider string) {
switch provider {
case "ipify6":
return "ipify"
case "noip4", "noip6", "noip8245_4", "noip8245_6":
return "noip"
case "cycle":
return "all"
default:
return provider
}
}

11
internal/config/sources/env/resolver.go vendored Normal file
View File

@@ -0,0 +1,11 @@
package env
import (
"github.com/qdm12/ddns-updater/internal/resolver"
)
func (s *Source) readResolver() (settings resolver.Settings, err error) {
settings.Address = s.env.Get("RESOLVER_ADDRESS")
settings.Timeout, err = s.env.Duration("RESOLVER_TIMEOUT")
return settings, err
}

9
internal/config/sources/env/server.go vendored Normal file
View File

@@ -0,0 +1,9 @@
package env
import "github.com/qdm12/ddns-updater/internal/config/settings"
func (s *Source) readServer() (settings settings.Server, err error) {
settings.RootURL = s.env.Get("ROOT_URL")
settings.Port, err = s.env.Uint16Ptr("LISTENING_PORT") // TODO change to address
return settings, err
}

52
internal/config/sources/env/shoutrrr.go vendored Normal file
View File

@@ -0,0 +1,52 @@
package env
import (
"fmt"
"net/url"
"path"
"strings"
"github.com/qdm12/ddns-updater/internal/config/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readShoutrrr() (settings settings.Shoutrrr, err error) {
settings.Addresses = s.env.CSV("SHOUTRRR_ADDRESSES", env.ForceLowercase(false))
// Retro-compatibility: GOTIFY_URL and GOTIFY_TOKEN
gotifyURLString := s.env.Get("GOTIFY_URL", env.ForceLowercase(false))
if gotifyURLString != nil {
s.handleDeprecated("GOTIFY_URL", "SHOUTRRR_ADDRESSES")
gotifyURL, err := url.Parse(*gotifyURLString)
if err != nil {
return settings, fmt.Errorf("gotify URL: %w", err)
}
gotifyToken := s.env.String("GOTIFY_TOKEN", env.ForceLowercase(false))
s.handleDeprecated("GOTIFY_TOKEN", "SHOUTRRR_ADDRESSES")
gotifyShoutrrrAddress := gotifyURLTokenToShoutrrr(gotifyURL, gotifyToken)
settings.Addresses = append(settings.Addresses, gotifyShoutrrrAddress)
}
paramsCSV := s.env.Get("SHOUTRRR_PARAMS", env.ForceLowercase(false))
if paramsCSV != nil {
keyValuePairs := strings.Split(*paramsCSV, ",")
settings.Params = make(map[string]string, len(keyValuePairs))
for _, keyValuePair := range keyValuePairs {
fields := strings.Split(keyValuePair, "=")
key, value := fields[0], fields[1]
settings.Params[key] = value
}
}
return settings, nil
}
func gotifyURLTokenToShoutrrr(url *url.URL, token string) (address string) {
hostAndPath := path.Join(url.Host, url.Path)
address = "gotify://" + hostAndPath + "/" + token
if url.Scheme == "http" {
address += "?DisableTLS=Yes"
}
return address
}

77
internal/config/sources/env/source.go vendored Normal file
View File

@@ -0,0 +1,77 @@
package env
import (
"fmt"
"os"
"github.com/qdm12/ddns-updater/internal/config/settings"
"github.com/qdm12/gosettings/sources/env"
)
type Source struct {
env env.Env
handleDeprecated func(deprecatedKey, currentKey string)
}
func New(warner Warner) *Source {
handleDeprecated := func(deprecatedKey, currentKey string) {
warner.Warnf("You are using an old environment variable %s, please change it to %s",
deprecatedKey, currentKey)
}
return &Source{
env: *env.New(os.Environ(), handleDeprecated),
handleDeprecated: handleDeprecated,
}
}
func (s *Source) Read() (settings settings.Settings, err error) {
settings.Client, err = s.readClient()
if err != nil {
return settings, fmt.Errorf("reading client settings: %w", err)
}
settings.Update, err = s.readUpdate()
if err != nil {
return settings, fmt.Errorf("reading update settings: %w", err)
}
settings.PubIP, err = s.readPubIP()
if err != nil {
return settings, fmt.Errorf("reading public IP settings: %w", err)
}
settings.Resolver, err = s.readResolver()
if err != nil {
return settings, fmt.Errorf("reading resolver settings: %w", err)
}
settings.IPv6, err = s.readIPv6()
if err != nil {
return settings, fmt.Errorf("reading IPv6 settings: %w", err)
}
settings.Server, err = s.readServer()
if err != nil {
return settings, fmt.Errorf("reading server settings: %w", err)
}
settings.Health = s.ReadHealth()
settings.Paths = s.readPaths()
settings.Backup, err = s.readBackup()
if err != nil {
return settings, fmt.Errorf("reading backup settings: %w", err)
}
settings.Logger, err = s.readLogger()
if err != nil {
return settings, fmt.Errorf("reading logger settings: %w", err)
}
settings.Shoutrrr, err = s.readShoutrrr()
if err != nil {
return settings, fmt.Errorf("reading shoutrrr settings: %w", err)
}
return settings, nil
}

35
internal/config/sources/env/update.go vendored Normal file
View File

@@ -0,0 +1,35 @@
package env
import (
"strconv"
"time"
"github.com/qdm12/ddns-updater/internal/config/settings"
)
func (s *Source) readUpdate() (settings settings.Update, err error) {
settings.Period, err = s.readUpdatePeriod()
if err != nil {
return settings, err
}
settings.Cooldown, err = s.env.Duration("UPDATE_COOLDOWN_PERIOD")
return settings, err
}
func (s *Source) readUpdatePeriod() (period time.Duration, err error) {
// Retro-compatibility: DELAY variable name
delayStringPtr := s.env.Get("DELAY")
if delayStringPtr != nil {
s.handleDeprecated("DELAY", "UPDATE_PERIOD")
// Retro-compatibility: integer only, treated as seconds
delayInt, err := strconv.Atoi(*delayStringPtr)
if err == nil {
return time.Duration(delayInt) * time.Second, nil
}
return time.ParseDuration(*delayStringPtr)
}
return s.env.Duration("UPDATE_PERIOD")
}

View File

@@ -1,55 +0,0 @@
package config
import (
"fmt"
"strconv"
"time"
"github.com/qdm12/golibs/params"
)
type Update struct {
Period time.Duration
Cooldown time.Duration
}
func (u *Update) get(env params.Interface) (warning string, err error) {
warning, err = u.getPeriod(env)
if err != nil {
return warning, err
}
u.Cooldown, err = env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
if err != nil {
return "", fmt.Errorf("%w: for environment variable UPDATE_COOLDOWN_PERIOD", err)
}
return warning, nil
}
func (u *Update) getPeriod(env params.Interface) (warning string, err error) {
// Backward compatibility: DELAY
s, err := env.Get("DELAY", params.Compulsory())
if err == nil {
warning = "the environment variable DELAY should be changed to PERIOD"
// Backward compatibility: integer only, treated as seconds
n, err := strconv.Atoi(s)
if err == nil {
u.Period = time.Duration(n) * time.Second
return warning, nil
}
period, err := time.ParseDuration(s)
if err == nil {
u.Period = period
return warning, nil
}
}
u.Period, err = env.Duration("PERIOD", params.Default("10m"))
if err != nil {
return "", fmt.Errorf("%w: for environment variable PERIOD", err)
}
return "", err
}

View File

@@ -1,8 +0,0 @@
package config
func appendIfNotEmpty(slice []string, s string) (newSlice []string) {
if s == "" {
return slice
}
return append(slice, s)
}

View File

@@ -5,8 +5,8 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"time"
)
@@ -29,8 +29,13 @@ var ErrUnhealthy = errors.New("program is unhealthy")
// Query sends an HTTP request to the other instance of
// the program, and to its internal healthcheck server.
func (c *Client) Query(ctx context.Context, port uint16) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:"+strconv.Itoa(int(port)), nil)
func (c *Client) Query(ctx context.Context, listeningAddress string) error {
_, port, err := net.SplitHostPort(listeningAddress)
if err != nil {
return fmt.Errorf("splitting host and port from address: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:"+port, nil)
if err != nil {
return err
}

View File

@@ -1,7 +0,0 @@
package params
import "github.com/qdm12/golibs/params"
type envInterface interface {
Get(key string, options ...params.OptionSetter) (value string, err error)
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/qdm12/ddns-updater/internal/settings"
"github.com/qdm12/ddns-updater/internal/settings/constants"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/golibs/params"
)
type commonSettings struct {
@@ -68,10 +67,8 @@ func (r *Reader) getSettingsFromFile(filePath string) (
// If the settings are valid, they are written to the filePath.
func (r *Reader) getSettingsFromEnv(filePath string) (
allSettings []settings.Settings, warnings []string, err error) {
s, err := r.env.Get("CONFIG", params.CaseSensitiveValue())
if err != nil {
return nil, nil, fmt.Errorf("%w: for environment variable CONFIG", err)
} else if s == "" {
s := os.Getenv("CONFIG")
if s == "" {
return nil, nil, nil
}
r.logger.Info("reading JSON config from environment variable CONFIG")

View File

@@ -3,13 +3,10 @@ package params
import (
"io/fs"
"os"
"github.com/qdm12/golibs/params"
)
type Reader struct {
logger Logger
env envInterface
readFile func(filename string) ([]byte, error)
writeFile func(filename string, data []byte, perm fs.FileMode) (err error)
}
@@ -22,7 +19,6 @@ type Logger interface {
func NewReader(logger Logger) *Reader {
return &Reader{
logger: logger,
env: params.New(),
readFile: os.ReadFile,
writeFile: os.WriteFile,
}

View File

@@ -3,8 +3,11 @@ package resolver
import (
"errors"
"fmt"
"net"
"os"
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/validate"
)
type Settings struct {
@@ -13,35 +16,27 @@ type Settings struct {
}
func (s *Settings) SetDefaults() {
if s.Address == nil {
s.Address = new(string)
}
s.Address = gosettings.DefaultPointer(s.Address, "")
const defaultTimeout = 5 * time.Second
s.Timeout = gosettings.DefaultNumber(s.Timeout, defaultTimeout)
}
if s.Timeout == 0 {
const defaultTimeout = 5 * time.Second
s.Timeout = defaultTimeout
}
func (s Settings) MergeWith(other Settings) (merged Settings) {
merged.Address = gosettings.MergeWithPointer(s.Address, other.Address)
merged.Timeout = gosettings.MergeWithNumber(s.Timeout, other.Timeout)
return merged
}
var (
ErrAddressHostEmpty = errors.New("address host is empty")
ErrAddressPortEmpty = errors.New("address port is empty")
ErrTimeoutTooLow = errors.New("timeout is too low")
ErrTimeoutTooLow = errors.New("timeout is too low")
)
func (s *Settings) Validate() (err error) {
func (s Settings) Validate() (err error) {
if *s.Address != "" {
host, port, err := net.SplitHostPort(*s.Address)
err = validate.ListeningAddress(*s.Address, os.Getuid())
if err != nil {
return fmt.Errorf("splitting host and port from address: %w", err)
}
switch {
case host == "":
return fmt.Errorf("%w: in %s", ErrAddressHostEmpty, *s.Address)
case port == "":
return fmt.Errorf("%w: in %s", ErrAddressPortEmpty, *s.Address)
}
}
const minTimeout = 10 * time.Millisecond