diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index 720fd92..f0ad493 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -15,21 +15,21 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6.0.2 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # ratchet:actions/setup-go@v6.3.0 with: go-version-file: 'go.mod' check-latest: true - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # ratchet:golangci/golangci-lint-action@v9.2.0 with: - version: v1.59 + version: latest govulncheck: name: govulncheck runs-on: ubuntu-latest steps: - id: govulncheck - uses: golang/govulncheck-action@v1 + uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # ratchet:golang/govulncheck-action@v1.0.4 with: go-version-file: 'go.mod' check-latest: true diff --git a/.golangci.yml b/.golangci.yml index 6101e3d..15f6269 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,22 +1,17 @@ -# Refer to golangci-lint's example config file for more options and information: -# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml - +version: "2" run: - timeout: 5m modules-download-mode: readonly linters: enable: - - errcheck - - gofmt - - goimports - gocyclo - - govet - misspell - revive - - staticcheck +formatters: + enable: + - gofmt + - goimports issues: - exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0 diff --git a/build/build.sh b/build/build.sh index d6fa4ef..c522cc9 100755 --- a/build/build.sh +++ b/build/build.sh @@ -18,14 +18,6 @@ showUsage() { exit 0 } -## install gox if needed -ensureGox() { - if ! which gox &> /dev/null; then - printf "Installing gox ... " - go install github.com/mitchellh/gox@v1 - fi -} - ## we require module support export GO111MODULE=on @@ -48,7 +40,6 @@ while getopts ":dcri" opt; do should_exit=true ;; r) - ensureGox RELEASE_BUILD=1 ;; i) @@ -73,15 +64,34 @@ if [ $RELEASE_BUILD -eq 1 ]; then ## clean dist directory rm -rf ./dist/ - ## call gox to build our binaries - CGO_ENABLED=0 gox \ - -osarch="linux/amd64 darwin/amd64 freebsd/amd64 openbsd/amd64 windows/amd64 windows/386" \ - -ldflags="-X main.appVersion=${RELEASE_VERSION} -s -w" \ - -output="./dist/${BUILD_NAME}-${RELEASE_VERSION}-{{.Arch}}-{{.OS}}/${BUILD_NAME}-${RELEASE_VERSION}" \ - > /dev/null >&1 - - ## gox return - RETURN_VALUE=$? + ## build release binaries with a shell loop instead of gox + RETURN_VALUE=0 + + for target in \ + linux/amd64 \ + darwin/amd64 \ + freebsd/amd64 \ + openbsd/amd64 \ + windows/amd64 \ + windows/386 + do + GOOS="${target%/*}" + GOARCH="${target#*/}" + OUTPUT_DIR="./dist/${BUILD_NAME}-${RELEASE_VERSION}-${GOARCH}-${GOOS}" + + mkdir -p "${OUTPUT_DIR}" + + CGO_ENABLED=0 GOOS="${GOOS}" GOARCH="${GOARCH}" \ + go build \ + -ldflags="-X main.appVersion=${RELEASE_VERSION} -s -w" \ + -o "${OUTPUT_DIR}/${BUILD_NAME}-${RELEASE_VERSION}" \ + > /dev/null 2>&1 + + RETURN_VALUE=$? + if [ ${RETURN_VALUE} -ne 0 ]; then + break + fi + done else diff --git a/build/codeCheck.sh b/build/codeCheck.sh index 70d79a8..ddd9442 100755 --- a/build/codeCheck.sh +++ b/build/codeCheck.sh @@ -4,8 +4,7 @@ golangci="$(go env GOPATH)/bin/golangci-lint" if ! ${golangci} version &> /dev/null; then echo "Installing golangci-lint binary to ${golangci}" - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ - sh -s -- -b "$(go env GOPATH)/bin" v1.57.1 + curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin echo "done" fi diff --git a/command/check/util.go b/command/check/util.go index ed172f6..f4cdc73 100644 --- a/command/check/util.go +++ b/command/check/util.go @@ -1,12 +1,13 @@ package check import ( + stderrors "errors" "flag" "fmt" "os" "github.com/leprechau/ipman/internal" - "github.com/leprechau/ipman/internal/errors" + internalerrors "github.com/leprechau/ipman/internal/errors" ) // setupFlags initializes the instance configuration @@ -21,7 +22,8 @@ func (c *Command) setupFlags(args []string) error { // init flagset cmdFlags = flag.NewFlagSet("check", flag.ContinueOnError) - cmdFlags.Usage = func() { _, _ = fmt.Fprint(os.Stdout, c.Help()); os.Exit(0) } + cmdFlags.SetOutput(os.Stdout) + cmdFlags.Usage = func() { _, _ = os.Stdout.WriteString(c.Help()) } // declare flags cmdFlags.BoolVar(&c.config.v4, "4", false, @@ -33,12 +35,15 @@ func (c *Command) setupFlags(args []string) error { // parse flags and ignore error if err = cmdFlags.Parse(args); err != nil { - return nil + if stderrors.Is(err, flag.ErrHelp) { + return nil + } + return fmt.Errorf("parse check flags: %w", err) } // check for remaining garbage if cmdFlags.NArg() > 0 { - return errors.ErrUnknownArg + return internalerrors.ErrUnknownArg } // default to v4 if not specified @@ -48,7 +53,7 @@ func (c *Command) setupFlags(args []string) error { // init ip backend if c.ip, err = internal.GetIPBackend(c.config.ipbe); err != nil { - return err + return fmt.Errorf("init ip backend: %w", err) } return nil diff --git a/command/check/util_test.go b/command/check/util_test.go new file mode 100644 index 0000000..b598938 --- /dev/null +++ b/command/check/util_test.go @@ -0,0 +1,60 @@ +package check + +import ( + "strings" + "testing" + + internalerrors "github.com/leprechau/ipman/internal/errors" +) + +func TestSetupFlagsDefaultsToIPv4(t *testing.T) { + t.Parallel() + + cmd := &Command{Self: "ipman"} + + if err := cmd.setupFlags(nil); err != nil { + t.Fatalf("setupFlags() error = %v", err) + } + + if !cmd.config.v4 { + t.Fatal("setupFlags() did not default to IPv4") + } + + if cmd.config.v6 { + t.Fatal("setupFlags() unexpectedly enabled IPv6") + } + + if cmd.ip == nil { + t.Fatal("setupFlags() did not initialize an IP backend") + } +} + +func TestSetupFlagsReturnsParseError(t *testing.T) { + t.Parallel() + + cmd := &Command{Self: "ipman"} + + err := cmd.setupFlags([]string{"-unknown"}) + if err == nil { + t.Fatal("setupFlags() error = nil, want parse failure") + } + + if !strings.Contains(err.Error(), "parse check flags") { + t.Fatalf("setupFlags() error = %v, want contextual parse error", err) + } +} + +func TestSetupFlagsRejectsTrailingArgs(t *testing.T) { + t.Parallel() + + cmd := &Command{Self: "ipman"} + + err := cmd.setupFlags([]string{"extra"}) + if err == nil { + t.Fatal("setupFlags() error = nil, want trailing arg failure") + } + + if err != internalerrors.ErrUnknownArg { + t.Fatalf("setupFlags() error = %v, want %v", err, internalerrors.ErrUnknownArg) + } +} diff --git a/command/update/util.go b/command/update/util.go index 62e855a..73dd586 100644 --- a/command/update/util.go +++ b/command/update/util.go @@ -1,12 +1,13 @@ package update import ( + stderrors "errors" "flag" "fmt" "os" "github.com/leprechau/ipman/internal" - "github.com/leprechau/ipman/internal/errors" + internalerrors "github.com/leprechau/ipman/internal/errors" ) // setupFlags initializes the instance configuration @@ -21,7 +22,8 @@ func (c *Command) setupFlags(args []string) error { // init flagset cmdFlags = flag.NewFlagSet("update", flag.ContinueOnError) - cmdFlags.Usage = func() { _, _ = fmt.Fprint(os.Stdout, c.Help()); os.Exit(0) } + cmdFlags.SetOutput(os.Stdout) + cmdFlags.Usage = func() { _, _ = os.Stdout.WriteString(c.Help()) } // declare flags cmdFlags.BoolVar(&c.config.v4, "4", false, @@ -45,18 +47,21 @@ func (c *Command) setupFlags(args []string) error { // parse flags and ignore error if err = cmdFlags.Parse(args); err != nil { - return nil + if stderrors.Is(err, flag.ErrHelp) { + return nil + } + return fmt.Errorf("parse update flags: %w", err) } // check for remaining garbage if cmdFlags.NArg() > 0 { - return errors.ErrUnknownArg + return internalerrors.ErrUnknownArg } // check zone and attempt to get from environment if c.config.zone == "" { if c.config.zone = os.Getenv("IPMAN_DNS_ZONE"); c.config.zone == "" { - return errors.ErrMissingZone + return internalerrors.ErrMissingZone } } @@ -77,12 +82,12 @@ func (c *Command) setupFlags(args []string) error { // init ip backend if c.ip, err = internal.GetIPBackend(c.config.ipbe); err != nil { - return err + return fmt.Errorf("init ip backend: %w", err) } // init dns backend if c.dns, err = internal.GetDNSBackend(c.config.dnsbe); err != nil { - return err + return fmt.Errorf("init dns backend: %w", err) } // set backend access key if needed diff --git a/command/update/util_test.go b/command/update/util_test.go new file mode 100644 index 0000000..d2a64f5 --- /dev/null +++ b/command/update/util_test.go @@ -0,0 +1,60 @@ +package update + +import ( + "strings" + "testing" + + internalerrors "github.com/leprechau/ipman/internal/errors" +) + +func TestSetupFlagsLoadsZoneFromEnv(t *testing.T) { + t.Setenv("IPMAN_DNS_ZONE", "example.com") + + cmd := &Command{Self: "ipman"} + + if err := cmd.setupFlags(nil); err != nil { + t.Fatalf("setupFlags() error = %v", err) + } + + if got := cmd.config.zone; got != "example.com" { + t.Fatalf("setupFlags() zone = %q, want %q", got, "example.com") + } + + if !cmd.config.v4 { + t.Fatal("setupFlags() did not default to IPv4") + } + + if cmd.dns == nil || cmd.ip == nil { + t.Fatal("setupFlags() did not initialize backends") + } +} + +func TestSetupFlagsReturnsParseError(t *testing.T) { + t.Setenv("IPMAN_DNS_ZONE", "example.com") + + cmd := &Command{Self: "ipman"} + + err := cmd.setupFlags([]string{"-ttl=bad"}) + if err == nil { + t.Fatal("setupFlags() error = nil, want parse failure") + } + + if !strings.Contains(err.Error(), "parse update flags") { + t.Fatalf("setupFlags() error = %v, want contextual parse error", err) + } +} + +func TestSetupFlagsRequiresZoneWhenUnset(t *testing.T) { + t.Parallel() + + cmd := &Command{Self: "ipman"} + + err := cmd.setupFlags(nil) + if err == nil { + t.Fatal("setupFlags() error = nil, want missing zone failure") + } + + if err != internalerrors.ErrMissingZone { + t.Fatalf("setupFlags() error = %v, want %v", err, internalerrors.ErrMissingZone) + } +} diff --git a/commands.go b/commands.go index 797b5c0..f3193c3 100644 --- a/commands.go +++ b/commands.go @@ -1,28 +1,36 @@ package main import ( + "log/slog" "os" "github.com/leprechau/ipman/command/check" "github.com/leprechau/ipman/command/update" - "github.com/mitchellh/cli" ) -// init command factory -func initCommands() map[string]cli.CommandFactory { - // register sub commands - return map[string]cli.CommandFactory{ - "check": func() (cli.Command, error) { +type runner interface { + Run([]string) int +} + +type commandFactory func() runner + +func initCommands(logger *slog.Logger) map[string]commandFactory { + return map[string]commandFactory{ + "check": func() runner { return &check.Command{ Self: os.Args[0], - Log: log, - }, nil + Log: logger, + } }, - "update": func() (cli.Command, error) { + "update": func() runner { return &update.Command{ Self: os.Args[0], - Log: log, - }, nil + Log: logger, + } }, } } + +func runCommand(args []string, newCommand commandFactory) int { + return newCommand().Run(args) +} diff --git a/go.mod b/go.mod index 4188d6e..4110c87 100644 --- a/go.mod +++ b/go.mod @@ -2,32 +2,6 @@ module github.com/leprechau/ipman go 1.21 -require ( - github.com/go-resty/resty/v2 v2.13.1 - github.com/mitchellh/cli v1.1.5 -) +require github.com/go-resty/resty/v2 v2.13.1 -require ( - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.3 // indirect - github.com/armon/go-radix v1.0.0 // indirect - github.com/bgentry/speakeasy v0.2.0 // indirect - github.com/fatih/color v1.17.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/huandu/xstrings v1.5.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/posener/complete v1.2.3 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect -) +require golang.org/x/net v0.27.0 // indirect diff --git a/go.sum b/go.sum index ccf77da..a037da7 100644 --- a/go.sum +++ b/go.sum @@ -1,104 +1,15 @@ -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= -github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= -github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= @@ -109,23 +20,16 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= @@ -133,7 +37,6 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -145,9 +48,3 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/backend.go b/internal/backend.go index bcf8aea..162023a 100644 --- a/internal/backend.go +++ b/internal/backend.go @@ -2,6 +2,8 @@ package internal import ( + "fmt" + // dns backends "github.com/leprechau/ipman/internal/dns" "github.com/leprechau/ipman/internal/dns/cloudflare" @@ -16,20 +18,20 @@ import ( func GetIPBackend(backend string) (ip.Backend, error) { switch backend { case "ipify": - return ipify.DefaultConfig() + return ipify.DefaultConfig(), nil case "local": - return local.DefaultConfig() + return local.DefaultConfig(), nil } - return nil, errors.ErrUnknownIPBackend + return nil, fmt.Errorf("%w: %s", errors.ErrUnknownIPBackend, backend) } // GetDNSBackend returns an initialized DNS backend of the requested type func GetDNSBackend(backend string) (dns.Backend, error) { switch backend { case "cloudflare": - return cloudflare.DefaultConfig() + return cloudflare.DefaultConfig(), nil case "godaddy": - return godaddy.DefaultConfig() + return godaddy.DefaultConfig(), nil } - return nil, errors.ErrUnknownDNSBackend + return nil, fmt.Errorf("%w: %s", errors.ErrUnknownDNSBackend, backend) } diff --git a/internal/dns/cloudflare/cloudflare.go b/internal/dns/cloudflare/cloudflare.go index 1eafd36..7399292 100644 --- a/internal/dns/cloudflare/cloudflare.go +++ b/internal/dns/cloudflare/cloudflare.go @@ -14,6 +14,8 @@ func (c *Config) Get(zone, name string, typ dns.RType) (string, error) { var err error var r *resty.Response + c.recordID = "" + // cloudflare punycode mapping seems broken - lookup the zone apex instead if name == "@" { if r, err = c.client.R(). @@ -67,10 +69,9 @@ func (c *Config) Get(zone, name string, typ dns.RType) (string, error) { // Upsert a record by name func (c *Config) Upsert(zone, name, data string, typ dns.RType) (string, error) { - r, err := c.client.R(). + req := c.client.R(). SetAuthToken(c.apiToken). SetPathParam("zone", zone). - SetPathParam("record", c.recordID). SetBody(&DNSRecord{ Content: data, Name: name, @@ -79,8 +80,20 @@ func (c *Config) Upsert(zone, name, data string, typ dns.RType) (string, error) TTL: c.recordTTL, }). SetResult(&DNSUpdateResponse{}). - SetError(&DNSErrorResponse{}). - Patch("/zones/{zone}/dns_records/{record}") + SetError(&DNSErrorResponse{}) + + var ( + r *resty.Response + err error + ) + + if c.recordID == "" { + r, err = req.Post("/zones/{zone}/dns_records") + } else { + r, err = req. + SetPathParam("record", c.recordID). + Patch("/zones/{zone}/dns_records/{record}") + } if err != nil { return "", err diff --git a/internal/dns/cloudflare/cloudflare_test.go b/internal/dns/cloudflare/cloudflare_test.go new file mode 100644 index 0000000..1e0efa9 --- /dev/null +++ b/internal/dns/cloudflare/cloudflare_test.go @@ -0,0 +1,147 @@ +package cloudflare + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "github.com/leprechau/ipman/internal/dns" +) + +func TestGetClearsStaleRecordIDAndUpsertCreatesRecord(t *testing.T) { + t.Parallel() + + var ( + mu sync.Mutex + methods []string + paths []string + ) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + methods = append(methods, r.Method) + paths = append(paths, r.URL.Path) + mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + + switch { + case r.Method == http.MethodGet && r.URL.Path == "/zones/test-zone/dns_records": + _ = json.NewEncoder(w).Encode(DNSResponse{ + Success: true, + Result: []DNSRecord{}, + ResultInfo: ResponseInfo{ + Count: 0, + }, + }) + case r.Method == http.MethodPost && r.URL.Path == "/zones/test-zone/dns_records": + var record DNSRecord + if err := json.NewDecoder(r.Body).Decode(&record); err != nil { + t.Fatalf("decode request body: %v", err) + } + + if record.Content != "1.2.3.4" { + t.Fatalf("POST content = %q, want %q", record.Content, "1.2.3.4") + } + + _ = json.NewEncoder(w).Encode(DNSUpdateResponse{ + Success: true, + Result: DNSRecord{ + Name: record.Name, + }, + }) + default: + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + })) + defer server.Close() + + cfg := DefaultConfig() + cfg.client.SetBaseURL(server.URL) + cfg.recordID = "stale-id" + + got, err := cfg.Get("test-zone", "www.example.com", dns.A) + if err != nil { + t.Fatalf("Get() error = %v", err) + } + + if got != "" { + t.Fatalf("Get() = %q, want empty result for missing record", got) + } + + if cfg.recordID != "" { + t.Fatalf("recordID after Get() = %q, want cleared stale value", cfg.recordID) + } + + name, err := cfg.Upsert("test-zone", "www.example.com", "1.2.3.4", dns.A) + if err != nil { + t.Fatalf("Upsert() error = %v", err) + } + + if name != "www.example.com" { + t.Fatalf("Upsert() = %q, want %q", name, "www.example.com") + } + + mu.Lock() + defer mu.Unlock() + + if len(methods) != 2 { + t.Fatalf("request count = %d, want 2", len(methods)) + } + + if methods[0] != http.MethodGet || methods[1] != http.MethodPost { + t.Fatalf("methods = %v, want [GET POST]", methods) + } + + if strings.Contains(paths[1], "stale-id") { + t.Fatalf("POST path = %q, stale record id leaked into create path", paths[1]) + } +} + +func TestUpsertUpdatesExistingRecord(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Fatalf("method = %s, want PATCH", r.Method) + } + + if r.URL.Path != "/zones/test-zone/dns_records/record-123" { + t.Fatalf("path = %q, want %q", r.URL.Path, "/zones/test-zone/dns_records/record-123") + } + + var record DNSRecord + if err := json.NewDecoder(r.Body).Decode(&record); err != nil { + t.Fatalf("decode request body: %v", err) + } + + if record.ID != "record-123" { + t.Fatalf("request body id = %q, want %q", record.ID, "record-123") + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(DNSUpdateResponse{ + Success: true, + Result: DNSRecord{ + Name: record.Name, + }, + }) + })) + defer server.Close() + + cfg := DefaultConfig() + cfg.client.SetBaseURL(server.URL) + cfg.recordID = "record-123" + + name, err := cfg.Upsert("test-zone", "www.example.com", "1.2.3.4", dns.A) + if err != nil { + t.Fatalf("Upsert() error = %v", err) + } + + if name != "www.example.com" { + t.Fatalf("Upsert() = %q, want %q", name, "www.example.com") + } +} diff --git a/internal/dns/cloudflare/config.go b/internal/dns/cloudflare/config.go index c1e3bb2..ba3f400 100644 --- a/internal/dns/cloudflare/config.go +++ b/internal/dns/cloudflare/config.go @@ -2,6 +2,7 @@ package cloudflare import ( "github.com/go-resty/resty/v2" + "github.com/leprechau/ipman/internal/dns" ) // Config contains backend configuration @@ -12,14 +13,16 @@ type Config struct { client *resty.Client } +var _ dns.Backend = (*Config)(nil) + // DefaultConfig returns the default backend configuration -func DefaultConfig() (*Config, error) { +func DefaultConfig() *Config { return &Config{ recordTTL: 600, client: resty.New(). SetHeader("Accept", "application/json"). SetBaseURL("https://api.cloudflare.com/client/v4"), - }, nil + } } // SetAccessKey sets the API access key diff --git a/internal/dns/godaddy/config.go b/internal/dns/godaddy/config.go index 89656c7..d443f4f 100644 --- a/internal/dns/godaddy/config.go +++ b/internal/dns/godaddy/config.go @@ -2,6 +2,7 @@ package godaddy import ( "github.com/go-resty/resty/v2" + "github.com/leprechau/ipman/internal/dns" ) // Config contains backend configuration @@ -12,14 +13,16 @@ type Config struct { client *resty.Client } +var _ dns.Backend = (*Config)(nil) + // DefaultConfig returns the default backend configuration -func DefaultConfig() (*Config, error) { +func DefaultConfig() *Config { return &Config{ recordTTL: 600, client: resty.New(). SetHeader("Accept", "application/json"). SetBaseURL("https://api.godaddy.com/v1"), - }, nil + } } // SetAccessKey sets the API access key diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 313fed0..73e758c 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -14,6 +14,9 @@ var ErrUnknownIPBackend = errors.New("unknown IP backend specified for command") // ErrUnknownDNSBackend is returned when an unknown DNS backend is specified on the cli var ErrUnknownDNSBackend = errors.New("unknown DNS backend specified for command") +// ErrUnknownIPProtocol is returned when an unsupported IP protocol is requested. +var ErrUnknownIPProtocol = errors.New("unknown IP protocol") + // ErrMissingZone is returned when no zone ID or domain name is specified on the cli var ErrMissingZone = errors.New("missing zone or domain name") diff --git a/internal/ip/ipify/config.go b/internal/ip/ipify/config.go index 40766f0..5f2756c 100644 --- a/internal/ip/ipify/config.go +++ b/internal/ip/ipify/config.go @@ -2,6 +2,7 @@ package ipify import ( "github.com/go-resty/resty/v2" + "github.com/leprechau/ipman/internal/ip" ) // Config contains backend configuration @@ -11,12 +12,14 @@ type Config struct { client *resty.Client } +var _ ip.Backend = (*Config)(nil) + // DefaultConfig returns the default backend configuration -func DefaultConfig() (*Config, error) { +func DefaultConfig() *Config { return &Config{ v4URL: "https://api.ipify.org", v6URL: "https://api6.ipify.org", client: resty.New(). SetHeader("Accept", "application/json"), - }, nil + } } diff --git a/internal/ip/local/config.go b/internal/ip/local/config.go index 54429cf..d49bb4c 100644 --- a/internal/ip/local/config.go +++ b/internal/ip/local/config.go @@ -1,16 +1,20 @@ package local +import "github.com/leprechau/ipman/internal/ip" + // Config contains backend configuration type Config struct { v4Host string v6Host string } +var _ ip.Backend = (*Config)(nil) + // DefaultConfig returns the default backend configuration -func DefaultConfig() (*Config, error) { +func DefaultConfig() *Config { // google public dns - the connection does not need to succeed return &Config{ v4Host: "8.8.8.8:53", v6Host: "[2001:4860:4860::8888]:53", - }, nil + } } diff --git a/internal/ip/local/local.go b/internal/ip/local/local.go index b6c98ea..64ac530 100644 --- a/internal/ip/local/local.go +++ b/internal/ip/local/local.go @@ -2,8 +2,10 @@ package local import ( + "fmt" "net" + "github.com/leprechau/ipman/internal/errors" "github.com/leprechau/ipman/internal/ip" ) @@ -22,6 +24,8 @@ func (c *Config) Get(proto ip.IFlag) (string, error) { if conn, err = net.Dial("udp6", c.v6Host); err != nil { return "", err } + default: + return "", fmt.Errorf("%w: %s", errors.ErrUnknownIPProtocol, proto) } // close after return diff --git a/main.go b/main.go index 9995b5f..aa70b4f 100644 --- a/main.go +++ b/main.go @@ -2,22 +2,17 @@ package main import ( + "fmt" "log/slog" "os" "strings" - - // CLI library - "github.com/mitchellh/cli" ) -// log is a package global logger -var log *slog.Logger - -// setupLogger configures the package global logger -func setupLogger() { +// setupLogger configures the application logger. +func setupLogger() *slog.Logger { lvl := &slog.LevelVar{} // create new level logger lvl.Set(slog.LevelInfo) // default to Info - log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})) + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})) if l := os.Getenv("LOG"); l != "" { switch { case strings.HasPrefix(l, "D"): @@ -28,26 +23,60 @@ func setupLogger() { lvl.Set(slog.LevelError) } } + + return logger } -// it all starts here -func main() { - var c *cli.CLI // cli object - var status int // exit status - var err error // general error holder +// usage generates the command-line usage message. +func usage(self string) string { + return fmt.Sprintf(`Usage: %s [options] + +Commands: + check Return current external IP address of local machine. + update Update DNS registry with external IP address of local machine. + help Show this help message. +`, self) +} + +func usageError(self, message string) string { + return fmt.Sprintf("%s: %s\n\n%s", self, message, usage(self)) +} - // setup logger - setupLogger() +// main is a thin wrapper around the real application entry point. +func main() { + os.Exit(realMain()) +} - // init and populate cli object - c = cli.NewCLI(appName, appVersion) - c.Args = os.Args[1:] // arguments minus command - c.Commands = initCommands() // see commands.go +// realMain executes the application and returns a process exit code. +func realMain() int { + logger := setupLogger() + commands := initCommands(logger) - // run command and check return - if status, err = c.Run(); err != nil { - log.Error("error executing CLI", "err", err) + if len(os.Args) < 2 { + _, _ = os.Stderr.WriteString(usage(os.Args[0])) + return 1 } - os.Exit(status) + switch arg := os.Args[1]; arg { + case "help", "-h", "--help": + _, _ = os.Stdout.WriteString(usage(os.Args[0])) + return 0 + default: + if strings.HasPrefix(arg, "-") { + message := fmt.Sprintf("unknown top-level flag %q", arg) + if arg == "-" || arg == "--" { + message = fmt.Sprintf("expected a subcommand before %q", arg) + } + + _, _ = os.Stderr.WriteString(usageError(os.Args[0], message)) + return 1 + } + + if cmd, ok := commands[arg]; ok { + return runCommand(os.Args[2:], cmd) + } + + _, _ = os.Stderr.WriteString(usageError(os.Args[0], fmt.Sprintf("unknown command %q", arg))) + return 1 + } } diff --git a/version.go b/version.go deleted file mode 100644 index 2b09907..0000000 --- a/version.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -// app name and version -var ( - appName = "ipman" - appVersion = "" -)