Skip to content

Commit 5ef7c35

Browse files
committed
Config refactoring
1 parent 3205f98 commit 5ef7c35

File tree

9 files changed

+257
-69
lines changed

9 files changed

+257
-69
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
MODULE := $(shell awk 'NR==1{print $$2}' go.mod)
44
REPO_NAME := cloudflare-ddns
55
VERSION := $(shell echo $$(ver=$$(git tag -l --points-at HEAD) && [ -z $$ver ] && ver=$$(git describe --always --dirty); printf $$ver))
6-
LDFLAGS := -s -w -X $(MODULE)/conf.Version=$(VERSION) -X $(MODULE)/conf.ModuleName=$(MODULE)
6+
LDFLAGS := -s -w -X $(MODULE)/meta.Version=$(VERSION) -X $(MODULE)/meta.ModuleName=$(MODULE)
77
FLAGS := -trimpath
88
PROJECT_ROOT := $(shell cd -P -- '$(shell dirname -- "$0")' && pwd -P)
99
BIN_NAME := cloudflare-ddns

cmd/root.go

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,91 +3,89 @@ package cmd
33
import (
44
"context"
55
"os"
6+
"path/filepath"
67
"strings"
78

89
"github.com/juju/errors"
910
"github.com/mattolenik/cloudflare-ddns-client/conf"
1011
"github.com/mattolenik/cloudflare-ddns-client/ddns"
1112
"github.com/mattolenik/cloudflare-ddns-client/errhandler"
13+
"github.com/mattolenik/cloudflare-ddns-client/meta"
1214
"github.com/rs/zerolog"
1315
"github.com/rs/zerolog/log"
1416
"github.com/spf13/cobra"
1517
"github.com/spf13/viper"
1618
)
1719

18-
var configFile string
19-
2020
// Root is the root command of the program
2121
var Root = &cobra.Command{
22-
Use: "cloudflare-ddns",
23-
Short: "Update a CloudFlare DNS record with your public IP",
22+
SilenceUsage: true,
23+
Use: meta.ProgramFilename,
24+
Short: "Update a CloudFlare DNS record with your public IP",
2425
Long: `A dynamic DNS client for CloudFlare. Automatically detects your public IP and
2526
creates/updates a DNS record in CloudFlare.
2627
2728
Configuration flags can be set by defining an environment variable of the same name.
2829
For example:
29-
` + "DOMAIN=mydomain.com RECORD=sub.mydomain.com TOKEN=<api-token> cloudflare-ddns" + `
30+
DOMAIN=mydomain.com RECORD=sub.mydomain.com TOKEN=<api-token> cloudflare-ddns
3031
`,
3132
RunE: func(cmd *cobra.Command, args []string) error {
32-
if viper.GetBool(conf.Daemon) {
33+
if conf.Daemon.Get() {
3334
return errors.Trace(ddns.DaemonWithDefaults(context.Background()))
3435
}
3536
return errors.Trace(ddns.Run(context.Background()))
3637
},
37-
Version: conf.Version,
38+
Version: meta.Version,
3839
}
3940

4041
func init() {
41-
Root.PersistentFlags().StringVar(&configFile, conf.Config, "", "Path to config file (default is /etc/cloudflare-ddns.toml)")
42-
Root.PersistentFlags().String(conf.Domain, "", "Domain name in CloudFlare, e.g. example.com")
43-
Root.PersistentFlags().String(conf.Record, "", "DNS record name in CloudFlare, may be subdomain or same as domain")
44-
Root.PersistentFlags().String(conf.Token, "", "CloudFlare API token with permissions Zone:Zone:Read and Zone:DNS:Edit")
45-
Root.PersistentFlags().Bool(conf.JSONOutput, false, "Log format, either pretty or json, defaults to pretty")
46-
Root.PersistentFlags().BoolP(conf.Verbose, conf.VerboseShort, false, "Verbose logging, prints additional log output")
47-
Root.PersistentFlags().Bool(conf.Daemon, false, "Run as a service, continually monitoring for DNS changes")
42+
if path, err := os.Executable(); err == nil {
43+
meta.ProgramDir = filepath.Dir(path)
44+
meta.ProgramFilename = filepath.Base(path)
45+
} else {
46+
panic(err)
47+
}
48+
f := Root.PersistentFlags()
49+
conf.Config.BindVar(f, &meta.ConfigFile).Viper()
50+
conf.Domain.Bind(f).Viper()
51+
conf.Record.Bind(f).Viper()
52+
conf.Token.Bind(f).Viper()
53+
conf.JSONOutput.Bind(f).Viper()
54+
conf.Verbose.Bind(f).Viper()
55+
conf.Daemon.Bind(f)
4856
Root.SetVersionTemplate("{{.Version}}\n")
4957

50-
viper.BindPFlag(conf.Config, Root.PersistentFlags().Lookup(conf.Config))
51-
viper.BindPFlag(conf.Domain, Root.PersistentFlags().Lookup(conf.Domain))
52-
viper.BindPFlag(conf.Record, Root.PersistentFlags().Lookup(conf.Record))
53-
viper.BindPFlag(conf.Token, Root.PersistentFlags().Lookup(conf.Token))
54-
viper.BindPFlag(conf.JSONOutput, Root.PersistentFlags().Lookup(conf.JSONOutput))
55-
56-
viper.SetDefault(conf.JSONOutput, false)
57-
viper.SetDefault(conf.Config, "/etc/cloudflare-ddns.toml")
58-
viper.SetDefault(conf.Verbose, false)
59-
6058
cobra.OnInitialize(initConfig)
6159
}
6260

6361
func initConfig() {
64-
if configFile != "" {
62+
if meta.ConfigFile != "" {
6563
// Use config file from the flag.
66-
viper.SetConfigFile(configFile)
64+
viper.SetConfigFile(meta.ConfigFile)
6765
} else {
68-
viper.AddConfigPath(".")
66+
viper.AddConfigPath(meta.ProgramDir)
6967
viper.AddConfigPath("$HOME/.config")
70-
viper.AddConfigPath("$HOME")
7168
viper.AddConfigPath("/etc")
72-
viper.SetConfigName("cloudflare-ddns.toml")
69+
viper.SetConfigName(meta.DefaultConfigFilename)
7370
}
7471
viper.AutomaticEnv()
7572
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
7673

7774
// Config file is optional, ignore errors
7875
err := viper.ReadInConfig()
79-
if !viper.GetBool(conf.JSONOutput) {
76+
// TODO: use enums/string consts instead of hardcoded string "json"
77+
if conf.JSONOutput.Get() != "json" {
8078
writer := zerolog.ConsoleWriter{Out: os.Stderr}
8179
log.Logger = log.Output(writer)
8280
}
83-
if viper.GetBool(conf.Verbose) {
81+
if conf.Verbose.Get() {
8482
zerolog.SetGlobalLevel(zerolog.DebugLevel)
8583
} else {
8684
zerolog.SetGlobalLevel(zerolog.InfoLevel)
8785
}
8886
// Config file is optional, continue if not found, unless config was specified by user and still not found
8987
_, notFound := err.(viper.ConfigFileNotFoundError)
90-
if !(notFound && configFile == "") {
88+
if !(notFound && meta.ConfigFile == "") {
9189
errhandler.Handle(err)
9290
}
9391
}

conf/conf.go

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,42 @@
11
package conf
22

3-
// Version is populated by the ldflags argument during build.
4-
var Version string = "TBD" // Set to TDB so it's visible when using go run
3+
import (
4+
"fmt"
55

6-
// ModuleName is the name of the Go module of this project, passed in by ldflags during build.
7-
var ModuleName string
6+
"github.com/mattolenik/cloudflare-ddns-client/meta"
7+
)
88

99
var (
10-
// Config is the path to the config file, if present
11-
Config = "config"
12-
// Daemon is the flag for enabling daemon mode
13-
Daemon = "daemon"
14-
// Domain is the domain to update within CloudFlare
15-
Domain = "domain"
16-
// Record is the DNS record to update within CloudFlare, may be same as Domain or a subdomain
17-
Record = "record"
18-
// Token is the CloudFlare API token
19-
Token = "token"
20-
// JSONOutput indicates that logging should be in JSON format instead of pretty console format
21-
JSONOutput = "json"
22-
// Verbose enables additional log output
23-
Verbose = "verbose"
24-
// VerboseShort is the short flag for verbose
25-
VerboseShort = "v"
10+
Config = StringOption{
11+
Name: "config",
12+
Description: fmt.Sprintf("Path to config file. If not specified will look for %s in the program dir (%s), $HOME/.config, or /etc, in that order", meta.DefaultConfigFilename, meta.ProgramDir),
13+
}
14+
Daemon = BoolOption{
15+
Name: "daemon",
16+
Default: false,
17+
Description: "Run as a service, continually monitoring for IP changes",
18+
}
19+
Domain = StringOption{
20+
Name: "domain",
21+
Description: "Domain name in CloudFlare, e.g. example.com",
22+
}
23+
Record = StringOption{
24+
Name: "record",
25+
Description: "DNS record name in CloudFlare, may be subdomain or same as domain",
26+
}
27+
Token = StringOption{
28+
Name: "token",
29+
Description: "CloudFlare API token with permissions Zone:Zone:Read and Zone:DNS:Edit",
30+
}
31+
JSONOutput = StringOption{
32+
Name: "log-format",
33+
Default: "pretty",
34+
Description: "Log format, either pretty or json, defaults to pretty",
35+
}
36+
Verbose = BoolOptionP{
37+
Name: "verbose",
38+
ShortName: "v",
39+
Description: "Verbose logging, prints additional log output",
40+
Default: true,
41+
}
2642
)

conf/opts.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package conf
2+
3+
import (
4+
"github.com/spf13/pflag"
5+
"github.com/spf13/viper"
6+
)
7+
8+
type StringOption struct {
9+
Name string
10+
Description string
11+
Default string
12+
flags *pflag.FlagSet
13+
}
14+
15+
func (o *StringOption) Get() string {
16+
return viper.GetString(o.Name)
17+
}
18+
19+
func (o *StringOption) Bind(flags *pflag.FlagSet) *StringOption {
20+
o.flags = flags
21+
flags.String(o.Name, o.Default, o.Description)
22+
return o
23+
}
24+
25+
func (o *StringOption) BindVar(flags *pflag.FlagSet, v *string) *StringOption {
26+
o.flags = flags
27+
flags.StringVar(v, o.Name, o.Default, o.Description)
28+
return o
29+
}
30+
31+
func (o *StringOption) Viper() {
32+
if o.flags == nil {
33+
panic("Must call Bind or BindVar before Viper")
34+
}
35+
viper.BindPFlag(o.Name, o.flags.Lookup(o.Name))
36+
viper.SetDefault(o.Name, o.Default)
37+
}
38+
39+
type StringOptionP struct {
40+
Name string
41+
ShortName string
42+
Description string
43+
Default string
44+
flags *pflag.FlagSet
45+
}
46+
47+
func (o *StringOptionP) Get() string {
48+
return viper.GetString(o.Name)
49+
}
50+
51+
func (o *StringOptionP) Bind(flags *pflag.FlagSet) *StringOptionP {
52+
o.flags = flags
53+
flags.String(o.Name, o.Default, o.Description)
54+
return o
55+
}
56+
57+
func (o *StringOptionP) BindVar(flags *pflag.FlagSet, v *string) *StringOptionP {
58+
o.flags = flags
59+
flags.StringVar(v, o.Name, o.Default, o.Description)
60+
return o
61+
}
62+
63+
func (o *StringOptionP) Viper() {
64+
if o.flags == nil {
65+
panic("Must call Bind or BindVar before Viper")
66+
}
67+
viper.BindPFlag(o.Name, o.flags.Lookup(o.Name))
68+
viper.SetDefault(o.Name, o.Default)
69+
}
70+
71+
type BoolOption struct {
72+
Name string
73+
Description string
74+
Default bool
75+
flags *pflag.FlagSet
76+
}
77+
78+
func (o *BoolOption) Get() bool {
79+
return viper.GetBool(o.Name)
80+
}
81+
82+
func (o *BoolOption) Bind(flags *pflag.FlagSet) *BoolOption {
83+
o.flags = flags
84+
flags.Bool(o.Name, o.Default, o.Description)
85+
return o
86+
}
87+
88+
func (o *BoolOption) BindVar(flags *pflag.FlagSet, v *bool) *BoolOption {
89+
o.flags = flags
90+
flags.BoolVar(v, o.Name, o.Default, o.Description)
91+
return o
92+
}
93+
94+
func (o *BoolOption) Viper() {
95+
if o.flags == nil {
96+
panic("Must call Bind or BindVar before Viper")
97+
}
98+
viper.BindPFlag(o.Name, o.flags.Lookup(o.Name))
99+
viper.SetDefault(o.Name, o.Default)
100+
}
101+
102+
type BoolOptionP struct {
103+
Name string
104+
ShortName string
105+
Description string
106+
Default bool
107+
flags *pflag.FlagSet
108+
}
109+
110+
func (o *BoolOptionP) Get() bool {
111+
return viper.GetBool(o.Name)
112+
}
113+
114+
func (o *BoolOptionP) Bind(flags *pflag.FlagSet) *BoolOptionP {
115+
o.flags = flags
116+
flags.BoolP(o.Name, o.ShortName, o.Default, o.Description)
117+
return o
118+
}
119+
120+
func (o *BoolOptionP) BindVar(flags *pflag.FlagSet, v *bool) *BoolOptionP {
121+
o.flags = flags
122+
flags.BoolVarP(v, o.Name, o.ShortName, o.Default, o.Description)
123+
return o
124+
}
125+
126+
func (o *BoolOptionP) Viper() {
127+
if o.flags == nil {
128+
panic("Must call Bind or BindVar before Viper")
129+
}
130+
viper.BindPFlag(o.Name, o.flags.Lookup(o.Name))
131+
viper.SetDefault(o.Name, o.Default)
132+
}

ddns/ddns.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/mattolenik/cloudflare-ddns-client/dns"
1010
"github.com/mattolenik/cloudflare-ddns-client/ip"
1111
"github.com/rs/zerolog/log"
12-
"github.com/spf13/viper"
1312
)
1413

1514
// Run performs a one time DDNS update.
@@ -21,9 +20,9 @@ func Run(ctx context.Context) error {
2120
log.Info().Msgf("Found public IP '%s'", ip)
2221
err = dns.UpdateCloudFlare(
2322
ctx,
24-
viper.GetString(conf.Token),
25-
viper.GetString(conf.Domain),
26-
viper.GetString(conf.Record),
23+
conf.Token.Get(),
24+
conf.Domain.Get(),
25+
conf.Record.Get(),
2726
ip)
2827
return errors.Trace(err)
2928
}
@@ -70,9 +69,9 @@ func Daemon(ctx context.Context, updatePeriod, failureRetryDelay time.Duration)
7069

7170
err = dns.UpdateCloudFlare(
7271
ctx,
73-
viper.GetString(conf.Token),
74-
viper.GetString(conf.Domain),
75-
viper.GetString(conf.Record),
72+
conf.Token.Get(),
73+
conf.Domain.Get(),
74+
conf.Record.Get(),
7675
lastIP)
7776
if err != nil {
7877
log.Error().Msgf("unable to update DDNS, will retry in %d seconds", updatePeriod/time.Second)

errhandler/handler.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/juju/errors"
88
"github.com/mattolenik/cloudflare-ddns-client/conf"
9+
"github.com/mattolenik/cloudflare-ddns-client/meta"
910
"github.com/rs/zerolog/log"
1011
"github.com/spf13/viper"
1112
)
@@ -16,11 +17,11 @@ func Handle(err error) {
1617
return
1718
}
1819
msg := errors.ErrorStack(err)
19-
if conf.ModuleName != "" {
20+
if meta.ModuleName != "" {
2021
// Remove name of module, makes stack traces shorter an easier to read
21-
msg = strings.ReplaceAll(msg, conf.ModuleName+"/", "")
22+
msg = strings.ReplaceAll(msg, meta.ModuleName+"/", "")
2223
}
23-
if viper.GetBool(conf.JSONOutput) {
24+
if viper.GetBool(conf.JSONOutput.Name) {
2425
// If writing to JSON logs, collapse stack trace into one line
2526
msg = strings.ReplaceAll(msg, "\n", " ↩ ")
2627
}

0 commit comments

Comments
 (0)