mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-08-02 18:48:49 -04:00
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:
@@ -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
3
go.mod
@@ -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
12
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
27
internal/config/settings/backup.go
Normal file
27
internal/config/settings/backup.go
Normal 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
|
||||
}
|
||||
25
internal/config/settings/client.go
Normal file
25
internal/config/settings/client.go
Normal 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
|
||||
}
|
||||
31
internal/config/settings/health.go
Normal file
31
internal/config/settings/health.go
Normal 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
|
||||
}
|
||||
3
internal/config/settings/helpers.go
Normal file
3
internal/config/settings/helpers.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package settings
|
||||
|
||||
const all = "all"
|
||||
25
internal/config/settings/ipv6.go
Normal file
25
internal/config/settings/ipv6.go
Normal 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
|
||||
}
|
||||
26
internal/config/settings/logger.go
Normal file
26
internal/config/settings/logger.go
Normal 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
|
||||
}
|
||||
22
internal/config/settings/paths.go
Normal file
22
internal/config/settings/paths.go
Normal 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
|
||||
}
|
||||
193
internal/config/settings/pubip.go
Normal file
193
internal/config/settings/pubip.go
Normal 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
|
||||
}
|
||||
38
internal/config/settings/server.go
Normal file
38
internal/config/settings/server.go
Normal 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
|
||||
}
|
||||
78
internal/config/settings/settings.go
Normal file
78
internal/config/settings/settings.go
Normal 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
|
||||
}
|
||||
37
internal/config/settings/shoutrrr.go
Normal file
37
internal/config/settings/shoutrrr.go
Normal 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
|
||||
}
|
||||
29
internal/config/settings/update.go
Normal file
29
internal/config/settings/update.go
Normal 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
|
||||
}
|
||||
@@ -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
16
internal/config/sources/env/backup.go
vendored
Normal 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
10
internal/config/sources/env/client.go
vendored
Normal 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
10
internal/config/sources/env/health.go
vendored
Normal 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
|
||||
}
|
||||
5
internal/config/sources/env/helpers.go
vendored
Normal file
5
internal/config/sources/env/helpers.go
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
package env
|
||||
|
||||
func ptrTo[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
5
internal/config/sources/env/interfaces.go
vendored
Normal file
5
internal/config/sources/env/interfaces.go
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
package env
|
||||
|
||||
type Warner interface {
|
||||
Warnf(format string, args ...any)
|
||||
}
|
||||
@@ -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")
|
||||
@@ -1,4 +1,4 @@
|
||||
package config
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
75
internal/config/sources/env/logger.go
vendored
Normal file
75
internal/config/sources/env/logger.go
vendored
Normal 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
10
internal/config/sources/env/paths.go
vendored
Normal 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
85
internal/config/sources/env/pubip.go
vendored
Normal 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
11
internal/config/sources/env/resolver.go
vendored
Normal 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
9
internal/config/sources/env/server.go
vendored
Normal 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
52
internal/config/sources/env/shoutrrr.go
vendored
Normal 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
77
internal/config/sources/env/source.go
vendored
Normal 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
35
internal/config/sources/env/update.go
vendored
Normal 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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package config
|
||||
|
||||
func appendIfNotEmpty(slice []string, s string) (newSlice []string) {
|
||||
if s == "" {
|
||||
return slice
|
||||
}
|
||||
return append(slice, s)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user