diff --git a/.github/workflows/spinifex.yml b/.github/workflows/spinifex.yml index 6b4a15b73..c185c252d 100644 --- a/.github/workflows/spinifex.yml +++ b/.github/workflows/spinifex.yml @@ -120,14 +120,11 @@ jobs: - name: Setup Go Workspace run: go work init ./spinifex ./viperblock ./predastore - - name: Check Format - run: make -C spinifex check-format + - name: Lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.11.3 + working-directory: spinifex - - name: Check Modernize - run: make -C spinifex check-modernize - - - name: Go Vet - run: make -C spinifex vet - - - name: Security Checks - run: make -C spinifex security-check + - name: Govulncheck + run: make -C spinifex govulncheck diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..3ee184fc4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,128 @@ +version: "2" + +formatters: + enable: + - gofmt + settings: + gofmt: + simplify: false # modernize linter covers gofmt -s simplifications (Go 1.23+) + +linters: + enable: + - asasalint # catches pass []any as any in variadic func(...any) + - asciicheck # checks that code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode bidi character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - canonicalheader # checks whether net/http.Header uses canonical header keys + - decorder # checks declaration order of types, consts, vars, and funcs + - durationcheck # checks for two durations multiplied together + - embeddedstructfieldcheck # checks embedded sync.Mutex/RWMutex in structs + - errcheck # checks for unchecked errors in type assertions and I/O + - errchkjson # checks types passed to json encoding functions, flags omittable error checks + - errname # checks that sentinel errors are prefixed with Err, error types suffixed with Error + - exptostd # detects golang.org/x/exp/ functions replaceable by stdlib + - fatcontext # detects nested contexts in loops + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - govet # reports suspicious constructs (Printf alignment, shadow, etc.) + - ineffassign # detects assignments to existing variables that are not used + - makezero # finds slice declarations with non-zero initial length followed by append + - mirror # catches wrong mirror patterns in bytes/strings usage + - modernize # modernizes code to use newer Go idioms (go fix analyzers) + - nakedret # finds naked returns in functions + - nilerr # catches code that returns nil even after checking err != nil + - nilnesserr # catches returning a different nil error variable after err != nil check + - nolintlint # reports ill-formed or insufficient nolint directives + - predeclared # catches shadowing of Go builtins (len, cap, error, etc.) + - reassign # checks that package variables are not reassigned + - recvcheck # checks for receiver type consistency (pointer vs value) + - sloglint # enforces consistent log/slog usage (key-value pairs, no globals) + - staticcheck # extensive static analysis checks + - unconvert # removes unnecessary type conversions + - usestdlibvars # detects hardcoded values that should use stdlib constants + - usetesting # catches os.TempDir() etc. in tests — should use t.TempDir() + - wastedassign # finds assignments to variables that are never subsequently read + - whitespace # detects leading and trailing whitespace in functions + + settings: + embeddedstructfieldcheck: + forbid-mutex: true + + errcheck: + check-type-assertions: true + exclude-functions: + - (*github.com/nats-io/nats.go.Subscription).Unsubscribe # cleanup-only, errors not actionable + - (*github.com/kdomanski/iso9660.ImageWriter).Cleanup # deferred cleanup, errors not actionable + + govet: + enable-all: true + disable: + - fieldalignment + - shadow # too noisy to retrofit — all 50 findings are err re-declarations + - unusedwrite # false positives on struct field assignments before marshal/send + + staticcheck: + checks: + - all + - -ST1000 # package comment + - -ST1003 # naming conventions (Id→ID etc.) — too noisy to retrofit + - -ST1012 # error var naming — errname covers this + error type suffixes + - -ST1013 # HTTP status constants — usestdlibvars is broader + - -ST1016 # receiver name consistency (too noisy to retrofit) + - -ST1020 # godoc on exported types — not enforcing yet + - -ST1021 # godoc on exported type groups — not enforcing yet + - -ST1022 # godoc on exported vars/consts — not enforcing yet + - -SA1019 # deprecated usage (handle separately) + - -SA4006 # value overwritten — ineffassign + wastedassign cover this better + - -SA9005 # ineffective break (staticcheck false positives) + + gosec: + excludes: + - G104 # unchecked errors — errcheck is stricter + - G116 # bidi chars — bidichk is more thorough + - G117 # use of net/http serve functions (we handle TLS separately) + - G703 # path traversal taint analysis — false positives on config file loading + - G705 # XSS taint analysis — false positives on internal error responses + - G706 # log injection taint analysis — false positives on structured slog usage + + modernize: + disable: + - newexpr # don't replace aws.String() with new() — AWS SDK idiom + - stringsbuilder # don't replace small string concatenations with strings.Builder + + nakedret: + max-func-lines: 0 + + nolintlint: + require-explanation: true + require-specific: true + + reassign: + patterns: + - ".*" + + exclusions: + presets: + - std-error-handling + - common-false-positives + rules: + - path: '_test\.go' + linters: + - bodyclose + - errcheck + - errchkjson + - gosec + + # spinifex-specific: gosec exclusions + - path: "cmd/" + linters: + - errcheck # CLI glue code — viper/cobra/pterm errors not actionable + - gosec # matches current -exclude-dir=cmd + + - text: "G204" + linters: [gosec] # subprocess execution — legitimate in VM orchestration + - text: "G304" + linters: [gosec] # file path from variable — necessary for config loading + - text: "G402" + linters: [gosec] # TLS config — we handle TLS ourselves diff --git a/Makefile b/Makefile index a5351e03a..17fceec58 100644 --- a/Makefile +++ b/Makefile @@ -67,10 +67,10 @@ go_run: @echo -e "\n....Running $(GO_PROJECT_NAME)...." $(GOPATH)/bin/$(GO_PROJECT_NAME) -# Preflight — runs the same checks as GitHub Actions (format + lint + security + tests). +# Preflight — runs the same checks as GitHub Actions (lint + vuln + tests). # Use this before committing to catch CI failures locally. preflight: - @$(MAKE) --no-print-directory QUIET=1 check-format check-modernize vet security-check test-cover diff-coverage test-race + @$(MAKE) --no-print-directory QUIET=1 lint govulncheck test-cover diff-coverage test-race @echo -e "\n ✅ Preflight passed — safe to commit." # Run unit tests @@ -143,59 +143,21 @@ quickinstall: install-system install-go install-aws @echo " Please ensure /usr/local/go/bin is in your PATH." @echo " Installed: Go ($(GO_ARCH)), AWS CLI ($(AWS_ARCH)), QEMU ($(QEMU_PACKAGES))" -# Format all Go files in place -format: - gofmt -w . +# Lint all Go code via golangci-lint (replaces check-format, vet, gosec, staticcheck) +lint: + @echo "Running golangci-lint..." + $(_Q)golangci-lint run ./... + @echo " golangci-lint ok" -# Check that all Go files are formatted (CI-compatible, fails on diff) -check-format: - @echo "Checking gofmt..." - @UNFORMATTED=$$(gofmt -l .); \ - if [ -n "$$UNFORMATTED" ]; then \ - echo "Files not formatted:"; \ - echo "$$UNFORMATTED"; \ - echo "Run 'make format' to fix."; \ - exit 1; \ - fi - @echo " gofmt ok" +# Auto-fix all linter issues that have fixers +fix: + golangci-lint run --fix ./... -# Go vet (fails on issues, matches CI) -vet: - @echo "Running go vet..." - $(_Q)go vet ./... - @echo " go vet ok" - -# Excluded: newexpr (replaces aws.String with new, not idiomatic for AWS SDK) -# Excluded: stringsbuilder (replaces string += in loops with strings.Builder, not worth the complexity for small loops) -GOFIX_EXCLUDE := -newexpr=false -stringsbuilder=false - -# Apply go fix modernizations -modernize: - @echo "Applying go fix modernizations..." - go fix $(GOFIX_EXCLUDE) ./... - @echo " go fix applied" - -# Check that code is modernized (CI-compatible, fails on diff) -check-modernize: - @echo "Checking go fix modernizations..." - @DIFF=$$(go fix $(GOFIX_EXCLUDE) -diff ./...); \ - if [ -n "$$DIFF" ]; then \ - echo "$$DIFF"; \ - echo "Run 'make modernize' to fix."; \ - exit 1; \ - fi - @echo " go fix ok" - -# Security checks — each tool fails the build on findings (matches CI). -# Reports are also saved to tests/ for review. -security-check: - @echo -e "\n....Running security checks for $(GO_PROJECT_NAME)...." - $(_Q)set -o pipefail && go tool govulncheck ./... $(_SECQ) tests/govulncheck-report.txt $(if $(QUIET),|| { cat tests/govulncheck-report.txt; exit 1; }) +# Govulncheck — dependency vulnerability scanning (not covered by golangci-lint) +govulncheck: + @echo "Running govulncheck..." + $(_Q)go tool govulncheck ./... @echo " govulncheck ok" - $(_Q)set -o pipefail && go tool gosec -quiet -exclude=G204,G304,G402,G117,G703,G705,G706 -exclude-generated -exclude-dir=cmd ./... $(_SECQ) tests/gosec-report.txt $(if $(QUIET),|| { cat tests/gosec-report.txt; exit 1; }) - @echo " gosec ok" - $(_Q)set -o pipefail && go tool staticcheck -checks="all,-ST1000,-ST1003,-ST1016,-ST1020,-ST1021,-ST1022,-SA1019,-SA9005" ./... $(_SECQ) tests/staticcheck-report.txt $(if $(QUIET),|| { cat tests/staticcheck-report.txt; exit 1; }) - @echo " staticcheck ok" # Build release tarballs — use distro-ARCH for single arch, distro for both distro: distro-amd64 distro-arm64 @@ -234,5 +196,5 @@ distro-clean: .PHONY: build build-ui go_build go_run preflight test test-cover test-race diff-coverage bench run clean \ install-system install-go install-aws quickinstall \ - format check-format modernize check-modernize vet security-check \ + lint fix govulncheck \ distro distro-amd64 distro-arm64 distro-clean diff --git a/cmd/spinifex/cmd/admin.go b/cmd/spinifex/cmd/admin.go index 5ff9b955f..45e49046e 100644 --- a/cmd/spinifex/cmd/admin.go +++ b/cmd/spinifex/cmd/admin.go @@ -238,11 +238,9 @@ func init() { imagesImportCmd.Flags().String("arch", "", "Specified distro arch (e.g aarch64, arm64, x86_64)") imagesImportCmd.Flags().String("platform", "Linux/UNIX", "Specified platform (e.g Linux/UNIX, Windows)") imagesImportCmd.Flags().Bool("force", false, "Force command execution (overwrites existing files)") - } func runimagesImportCmd(cmd *cobra.Command, args []string) { - var image utils.Images var imageFile string @@ -277,7 +275,6 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) { imageName, _ := cmd.Flags().GetString("name") if imageName != "" { - var exists bool // Confirm the image exists image, exists = utils.AvailableImages[imageName] @@ -286,9 +283,7 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) { fmt.Fprintf(os.Stderr, "Image name not found in available images") os.Exit(1) } - } else { - imageFile, _ = cmd.Flags().GetString("file") if imageFile == "" { @@ -305,7 +300,6 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) { image.Version, _ = cmd.Flags().GetString("version") image.Arch, _ = cmd.Flags().GetString("arch") image.Platform, _ = cmd.Flags().GetString("platform") - } if image.Distro == "" { @@ -352,14 +346,12 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) { if admin.FileExists(imageFile) && !forceCmd { fmt.Printf("Image file already exists, skipping download, use --force to overwrite: %s\n", imageFile) } else { - err := utils.DownloadFileWithProgress(image.URL, image.Name, imageFile, 0) if err != nil { fmt.Printf("Download failed: %v\n", err) os.Exit(1) } - } // Update image file path for later extraction @@ -476,12 +468,10 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) { defer os.RemoveAll(tmpDir) fmt.Printf("✅ Image import complete. Image-ID (AMI): %s\n", volumeId) - } // List remote images available func runimagesListCmd(cmd *cobra.Command, args []string) { - //fmt.Println(availableImages) tableData := pterm.TableData{ @@ -500,7 +490,6 @@ func runimagesListCmd(cmd *cobra.Command, args []string) { // 3. Iterate in sorted order for _, k := range keys { - img := utils.AvailableImages[k] //for _, img := range utils.AvailableImages { @@ -790,7 +779,6 @@ func runAdminInit(cmd *cobra.Command, args []string) { func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID, natsToken, clusterName, configDir, spxRoot, certPath, region, az, node, bindIP, clusterBind string, port, expectedNodes int, formationTimeoutStr string, services []string) { - formationTimeout, err := time.ParseDuration(formationTimeoutStr) if err != nil { fmt.Fprintf(os.Stderr, "❌ Error: Invalid --formation-timeout: %v\n", err) diff --git a/cmd/spinifex/cmd/get.go b/cmd/spinifex/cmd/get.go index 2e5126597..cb3c72704 100644 --- a/cmd/spinifex/cmd/get.go +++ b/cmd/spinifex/cmd/get.go @@ -236,6 +236,7 @@ func runGetVMs(cmd *cobra.Command, args []string) { type vmRow struct { types.VMInfo + Node string Host string } diff --git a/cmd/spinifex/cmd/root.go b/cmd/spinifex/cmd/root.go index f2ba33d9e..b9b4bcfc5 100644 --- a/cmd/spinifex/cmd/root.go +++ b/cmd/spinifex/cmd/root.go @@ -102,7 +102,6 @@ func init() { //viper.BindPFlag("nats.host", rootCmd.PersistentFlags().Lookup("nats-host")) //viper.BindPFlag("nats.acl.token", rootCmd.PersistentFlags().Lookup("nats-token")) //viper.BindPFlag("nats.sub.subject", rootCmd.PersistentFlags().Lookup("nats-subject")) - } // initConfig reads in config file and ENV variables if set. diff --git a/cmd/spinifex/cmd/service.go b/cmd/spinifex/cmd/service.go index 88db7dec8..dba5c7222 100644 --- a/cmd/spinifex/cmd/service.go +++ b/cmd/spinifex/cmd/service.go @@ -744,7 +744,6 @@ var vpcdStatusCmd = &cobra.Command{ } func init() { - viper.SetEnvPrefix("SPINIFEX") // Prefix for environment variables viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) @@ -928,5 +927,4 @@ func init() { vpcdCmd.AddCommand(vpcdStartCmd) vpcdCmd.AddCommand(vpcdStopCmd) vpcdCmd.AddCommand(vpcdStatusCmd) - } diff --git a/go.mod b/go.mod index 8cd0b2f03..2905243e2 100644 --- a/go.mod +++ b/go.mod @@ -25,17 +25,11 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.18.2 // indirect - cloud.google.com/go/compute/metadata v0.9.0 // indirect - github.com/BurntSushi/toml v1.6.0 // indirect - github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect github.com/antithesishq/antithesis-sdk-go v0.6.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boltdb/bolt v1.3.1 // indirect github.com/buraksezer/consistent v0.10.0 // indirect - github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/hub v1.0.2 // indirect github.com/cenkalti/rpc2 v1.0.5 // indirect @@ -47,7 +41,6 @@ require ( github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -57,13 +50,8 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/flatbuffers v25.12.19+incompatible // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-tpm v0.9.8 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.13 // indirect - github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gookit/color v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-metrics v0.5.4 // indirect @@ -87,7 +75,6 @@ require ( github.com/nats-io/jwt/v2 v2.8.0 // indirect github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/openai/openai-go/v3 v3.24.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect @@ -95,26 +82,20 @@ require ( github.com/prometheus/procfs v0.20.1 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/securego/gosec/v2 v2.24.7 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.2.0 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.etcd.io/bbolt v1.4.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect go.opentelemetry.io/otel v1.41.0 // indirect go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.48.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20260218203240-3dfff04db8fa // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect @@ -124,17 +105,10 @@ require ( golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.42.0 // indirect + golang.org/x/tools/go/expect v0.1.1-deprecated // indirect golang.org/x/vuln v1.1.4 // indirect - google.golang.org/genai v1.49.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - honnef.co/go/tools v0.7.0 // indirect ) -tool ( - github.com/securego/gosec/v2/cmd/gosec - golang.org/x/vuln/cmd/govulncheck - honnef.co/go/tools/cmd/staticcheck -) +tool golang.org/x/vuln/cmd/govulncheck diff --git a/go.sum b/go.sum index a6f1517a1..c53c47db5 100644 --- a/go.sum +++ b/go.sum @@ -7,14 +7,6 @@ atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtE atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= -cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= @@ -25,15 +17,11 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= -github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/antithesishq/antithesis-sdk-go v0.6.0 h1:v/YViLhFYkZOEEof4AXjD5AgGnGM84YHF4RqEwp6I2g= github.com/antithesishq/antithesis-sdk-go v0.6.0/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= @@ -87,8 +75,6 @@ github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU= github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw= -github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= -github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/hub v1.0.2 h1:Nqv9TNaA9boeO2wQFW8o87BY3zKthtnzXmWGmJqhAV8= @@ -115,15 +101,11 @@ github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuM github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -152,8 +134,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -167,8 +147,6 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU= @@ -183,26 +161,16 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 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/googleapis/enterprise-certificate-proxy v0.3.13 h1:hSPAhW3NX+7HNlTsmrvU0jL75cIzxFktheceg95Nq14= -github.com/googleapis/enterprise-certificate-proxy v0.3.13/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= -github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= @@ -305,12 +273,6 @@ github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= -github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= -github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/ovn-kubernetes/libovsdb v0.8.1 h1:M2J8bcJt5mXCom0HqzfEtuHkT80CTSQRcYG7acT8gf4= github.com/ovn-kubernetes/libovsdb v0.8.1/go.mod h1:ZlnHLzagmLOSvyd9qfxBIZp6wOSOw0IsRsc+6lNUGbU= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -364,10 +326,6 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/securego/gosec/v2 v2.24.7 h1:3k5yJnrhT1TTdsG0ZsnenlfCcT+7Y/+zeCPHbL7QAn8= -github.com/securego/gosec/v2 v2.24.7/go.mod h1:AdDJbjcG/XxFgVv7pW19vMNYlFM6+Q6Qy3t6lWAUcEY= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -388,7 +346,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -403,17 +360,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= -github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -423,16 +369,10 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -451,8 +391,6 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= -golang.org/x/exp/typeparams v0.0.0-20260218203240-3dfff04db8fa h1:6Wi43P0isP8Nl8D4qJmA3VC4FswVH0RJkr5cauo67SQ= -golang.org/x/exp/typeparams v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= 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/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= @@ -542,15 +480,7 @@ golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= -google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -579,5 +509,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= -honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= -honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= diff --git a/spinifex/admin/admin.go b/spinifex/admin/admin.go index 3b39ebc95..83b8c7d73 100644 --- a/spinifex/admin/admin.go +++ b/spinifex/admin/admin.go @@ -81,7 +81,6 @@ type ConfigFile struct { } func GenerateConfigFiles(configs []ConfigFile, configSettings ConfigSettings) error { - for _, cfg := range configs { if err := GenerateConfigFile(cfg.Path, cfg.Template, configSettings); err != nil { return fmt.Errorf("error creating %s: %v", cfg.Name, err) @@ -178,7 +177,6 @@ func GenerateServerCertOnly(configDir string, bindIP string) error { } func CreateServiceDirectories(spxRoot string) { - // Create additional directories dirs := []string{ filepath.Join(spxRoot, "images"), @@ -194,7 +192,6 @@ func CreateServiceDirectories(spxRoot string) { fmt.Println("\n📁 Creating directory structure...") for _, dir := range dirs { - // Check if directory exists if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0750); err != nil { @@ -203,7 +200,6 @@ func CreateServiceDirectories(spxRoot string) { } } fmt.Printf("✅ Directory structure created in %s\n", spxRoot) - } // Helper functions @@ -239,7 +235,8 @@ func ChownRecursive(path, username string) { _ = fs.WalkDir(root.FS(), ".", func(p string, d fs.DirEntry, err error) error { if err != nil { - return nil + slog.Debug("chown walk: skipping inaccessible entry", "path", p, "err", err) + return nil // best-effort: continue past inaccessible entries } fullPath := filepath.Join(path, p) if chownErr := os.Lchown(fullPath, uid, gid); chownErr != nil { diff --git a/spinifex/config/config.go b/spinifex/config/config.go index dee6cf6d1..2133783c3 100644 --- a/spinifex/config/config.go +++ b/spinifex/config/config.go @@ -148,7 +148,6 @@ func LoadConfig(configPath string) (*ClusterConfig, error) { return nil, fmt.Errorf("error reading config file: %w", err) } //fmt.Fprintf(os.Stderr, "Using config file: %s\n", viper.ConfigFileUsed()) - } else { fmt.Fprintf(os.Stderr, "Config file not found: %s, using environment variables and defaults\n", configPath) } diff --git a/spinifex/daemon/daemon.go b/spinifex/daemon/daemon.go index 2f1b86423..dd66d39db 100644 --- a/spinifex/daemon/daemon.go +++ b/spinifex/daemon/daemon.go @@ -302,13 +302,13 @@ func (rm *ResourceManager) GetResourceStats() (totalVCPU int, totalMemGB float64 remainingMem := rm.availableMem - rm.allocatedMem for _, it := range rm.instanceTypes { - cap := resourceStatsForType(remainingVCPU, remainingMem, it) - if cap.VCPU == 0 || cap.MemoryGB == 0 { + typeCap := resourceStatsForType(remainingVCPU, remainingMem, it) + if typeCap.VCPU == 0 || typeCap.MemoryGB == 0 { continue } - caps = append(caps, cap) + caps = append(caps, typeCap) } - return + return totalVCPU, totalMemGB, allocVCPU, allocMemGB, caps } // SetConfigPath sets the configuration file path for cluster management @@ -1131,7 +1131,6 @@ func (d *Daemon) saveClusterConfig() error { // ClusterManager starts the HTTP cluster management server func (d *Daemon) ClusterManager() error { - // Get daemon host from config daemonHost := d.config.Daemon.Host if daemonHost == "" { @@ -1145,7 +1144,7 @@ func (d *Daemon) ClusterManager() error { configHash, err := d.computeConfigHash() if err != nil { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"failed to compute config hash"}`)) return } @@ -1202,7 +1201,7 @@ func (d *Daemon) ClusterManager() error { var req types.NodeJoinRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) if err := json.NewEncoder(w).Encode(types.NodeJoinResponse{ Success: false, Message: "invalid request body", @@ -1217,7 +1216,7 @@ func (d *Daemon) ClusterManager() error { // Validate request if req.Node == "" || req.Region == "" || req.AZ == "" { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) if err := json.NewEncoder(w).Encode(types.NodeJoinResponse{ Success: false, Message: "node, region, and az are required", @@ -1230,7 +1229,7 @@ func (d *Daemon) ClusterManager() error { // Check if node already exists if _, exists := d.clusterConfig.Nodes[req.Node]; exists { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(409) + w.WriteHeader(http.StatusConflict) if err := json.NewEncoder(w).Encode(types.NodeJoinResponse{ Success: false, Message: fmt.Sprintf("node %s already exists in cluster", req.Node), @@ -1265,7 +1264,7 @@ func (d *Daemon) ClusterManager() error { if err := d.saveClusterConfig(); err != nil { slog.Error("Failed to save cluster config", "error", err) w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) if err := json.NewEncoder(w).Encode(types.NodeJoinResponse{ Success: false, Message: "failed to save cluster config", @@ -1405,7 +1404,6 @@ func (d *Daemon) LoadState() error { } func (d *Daemon) SendQMPCommand(q *qmp.QMPClient, cmd qmp.QMPCommand, instanceId string) (*qmp.QMPResponse, error) { - // Confirm QMP client is initialized if q == nil || q.Encoder == nil || q.Decoder == nil { return nil, fmt.Errorf("QMP client is not initialized") @@ -1419,7 +1417,7 @@ func (d *Daemon) SendQMPCommand(q *qmp.QMPClient, cmd qmp.QMPCommand, instanceId if err := q.Conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil { return nil, fmt.Errorf("set read deadline: %w", err) } - defer q.Conn.SetReadDeadline(time.Time{}) // clear deadline after command + defer func() { _ = q.Conn.SetReadDeadline(time.Time{}) }() // clear deadline after command if err := q.Encoder.Encode(cmd); err != nil { return nil, fmt.Errorf("encode error: %w", err) @@ -1446,7 +1444,10 @@ func (d *Daemon) SendQMPCommand(q *qmp.QMPClient, cmd qmp.QMPCommand, instanceId return nil, fmt.Errorf("QMP error: %s: %s", errObj["class"], errObj["desc"]) } if _, ok := msg["return"]; ok { - respBytes, _ := json.Marshal(msg) + respBytes, err := json.Marshal(msg) + if err != nil { + return nil, fmt.Errorf("marshal QMP response: %w", err) + } var resp qmp.QMPResponse if err := json.Unmarshal(respBytes, &resp); err != nil { return nil, fmt.Errorf("unmarshal error: %w", err) @@ -1457,15 +1458,12 @@ func (d *Daemon) SendQMPCommand(q *qmp.QMPClient, cmd qmp.QMPCommand, instanceId } func (d *Daemon) stopInstance(instances map[string]*vm.VM, deleteVolume bool) error { - // Signal to shutdown each VM var wg sync.WaitGroup // Run asynchronously within a worker group for _, instance := range instances { - wg.Go(func() { - // Send shutdown command - if it fails, VM may already be dead, continue with cleanup _, err := d.SendQMPCommand(instance.QMPClient, qmp.QMPCommand{Execute: "system_powerdown"}, instance.ID) if err != nil { @@ -1495,7 +1493,6 @@ func (d *Daemon) stopInstance(instances map[string]*vm.VM, deleteVolume bool) er defer instance.EBSRequests.Mu.Unlock() for _, ebsRequest := range instance.EBSRequests.Requests { - // Send the volume payload as JSON ebsUnMountRequest, err := json.Marshal(ebsRequest) @@ -1615,12 +1612,10 @@ func (d *Daemon) stopInstance(instances map[string]*vm.VM, deleteVolume bool) er } } return nil - } func (d *Daemon) setupShutdown() { d.shutdownWg.Go(func() { - sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) @@ -1650,7 +1645,6 @@ func (d *Daemon) setupShutdown() { if err := sub.Unsubscribe(); err != nil { slog.Error("Error unsubscribing from NATS", "err", err) } - } // Write shutdown marker to cluster state KV @@ -1682,7 +1676,6 @@ func (d *Daemon) setupShutdown() { } func (d *Daemon) CreateQMPClient(instance *vm.VM) (err error) { - // Create a new QMP client to communicate with the instance instance.QMPClient, err = qmp.NewQMPClient(instance.Config.QMPSocket) @@ -1734,11 +1727,9 @@ func (d *Daemon) CreateQMPClient(instance *vm.VM) (err error) { }() return nil - } func (d *Daemon) LaunchInstance(instance *vm.VM) (err error) { - // Abort if instance is no longer in a launchable state (e.g., terminated // by a concurrent request while waiting in the launch queue). d.Instances.Mu.Lock() @@ -1749,7 +1740,7 @@ func (d *Daemon) LaunchInstance(instance *vm.VM) (err error) { } // First, confirm if the instance is already running - pid, err := utils.ReadPidFile(instance.ID) + pid, _ := utils.ReadPidFile(instance.ID) if pid > 0 { process, err := os.FindProcess(pid) @@ -1974,7 +1965,6 @@ func (d *Daemon) startPendingWatchdog() { } func (d *Daemon) StartInstance(instance *vm.VM) error { - pidFile, err := utils.GeneratePidFile(instance.ID) if err != nil { @@ -2025,7 +2015,6 @@ func (d *Daemon) StartInstance(instance *vm.VM) error { instance.EBSRequests.Mu.Lock() for _, v := range instance.EBSRequests.Requests { - drive := vm.Drive{} // Use the NBDURI from mount response - contains socket path or TCP address @@ -2293,12 +2282,10 @@ func (d *Daemon) ebsTopic(action string) string { // MountVolumes mounts the volumes for an instance func (d *Daemon) MountVolumes(instance *vm.VM) error { - instance.EBSRequests.Mu.Lock() defer instance.EBSRequests.Mu.Unlock() for k, v := range instance.EBSRequests.Requests { - // Send the volume payload as JSON ebsMountRequest, err := json.Marshal(v) @@ -2327,21 +2314,17 @@ func (d *Daemon) MountVolumes(instance *vm.VM) error { } if ebsMountResponse.Error == "" { - slog.Debug("Mounted volume successfully", "response", ebsMountResponse.URI) // Append the NBD URI to the request instance.EBSRequests.Requests[k].NBDURI = ebsMountResponse.URI - } else { slog.Error("Failed to mount volume", "error", ebsMountResponse.Error) return fmt.Errorf("failed to mount volume: %s", ebsMountResponse.Error) } - } return nil - } // rollbackEBSMount sends an ebs.unmount request to undo a previously successful ebs.mount. diff --git a/spinifex/daemon/daemon_test.go b/spinifex/daemon/daemon_test.go index f41054e14..edb0da23d 100644 --- a/spinifex/daemon/daemon_test.go +++ b/spinifex/daemon/daemon_test.go @@ -31,12 +31,7 @@ import ( // createTestDaemon creates a test daemon instance with minimal configuration func createTestDaemon(t *testing.T, natsURL string) *Daemon { // Create a temporary directory for test data - tmpDir, err := os.MkdirTemp("", "spx-daemon-test-*") - require.NoError(t, err, "Failed to create temp directory") - - t.Cleanup(func() { - os.RemoveAll(tmpDir) - }) + tmpDir := t.TempDir() // New cluster config clusterCfg := &config.ClusterConfig{ @@ -345,9 +340,7 @@ func TestHandleEC2RunInstances_ResourceManagement(t *testing.T) { // TestDaemon_Initialization tests daemon initialization func TestDaemon_Initialization(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "spx-daemon-init-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) + tmpDir := t.TempDir() // New cluster config clusterCfg := &config.ClusterConfig{ @@ -859,9 +852,7 @@ func TestDaemon_BootAllocation(t *testing.T) { natsURL := sharedJSNATSURL // Create daemon temp directory - tmpDir, err := os.MkdirTemp("", "spx-daemon-boot-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) + tmpDir := t.TempDir() // Create test VMs with one running and one stopped instance vms := map[string]*vm.VM{ diff --git a/spinifex/daemon/network_test.go b/spinifex/daemon/network_test.go index 1990a1e28..c0a6ec93e 100644 --- a/spinifex/daemon/network_test.go +++ b/spinifex/daemon/network_test.go @@ -403,7 +403,7 @@ func TestGenerateDevMAC_Format(t *testing.T) { t.Errorf("generateDevMAC(%q) = %q, expected ':' at pos %d", id, mac, i) } } else { - if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { + if (c < '0' || c > '9') && (c < 'a' || c > 'f') { t.Errorf("generateDevMAC(%q) = %q, invalid hex char '%c' at pos %d", id, mac, c, i) } } diff --git a/spinifex/daemon/resource.go b/spinifex/daemon/resource.go index 30807c6fb..d016df346 100644 --- a/spinifex/daemon/resource.go +++ b/spinifex/daemon/resource.go @@ -16,7 +16,6 @@ var errInsufficientCapacity = errors.New("insufficient capacity to satisfy MinCo // or side effects. func canAllocateCount(availVCPU, allocVCPU int, availMem, allocMem float64, vCPUs int64, memMiB int64, maxCount int) int { - remainingVCPU := availVCPU - allocVCPU remainingMem := availMem - allocMem memoryGB := float64(memMiB) / 1024.0 diff --git a/spinifex/daemon/resource_test.go b/spinifex/daemon/resource_test.go index bf96844bf..6cb962a5d 100644 --- a/spinifex/daemon/resource_test.go +++ b/spinifex/daemon/resource_test.go @@ -201,11 +201,11 @@ func TestResourceStatsForType(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - cap := resourceStatsForType(tc.remainVCPU, tc.remainMem, tc.it) - assert.Equal(t, tc.wantName, cap.Name) - assert.Equal(t, tc.wantVCPU, cap.VCPU) - assert.InDelta(t, tc.wantMemGB, cap.MemoryGB, 0.001) - assert.Equal(t, tc.wantAvail, cap.Available) + typeCap := resourceStatsForType(tc.remainVCPU, tc.remainMem, tc.it) + assert.Equal(t, tc.wantName, typeCap.Name) + assert.Equal(t, tc.wantVCPU, typeCap.VCPU) + assert.InDelta(t, tc.wantMemGB, typeCap.MemoryGB, 0.001) + assert.Equal(t, tc.wantAvail, typeCap.Available) }) } } diff --git a/spinifex/daemon/state_test.go b/spinifex/daemon/state_test.go index af5127073..a774cb084 100644 --- a/spinifex/daemon/state_test.go +++ b/spinifex/daemon/state_test.go @@ -3,7 +3,6 @@ package daemon import ( "fmt" "net" - "os" "path/filepath" "sync" "testing" @@ -24,9 +23,7 @@ import ( func createDaemonWithJetStream(t *testing.T) *Daemon { t.Helper() - jsTmpDir, err := os.MkdirTemp("", "nats-js-state-test-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(jsTmpDir) }) + jsTmpDir := t.TempDir() opts := &server.Options{ Host: "127.0.0.1", @@ -44,9 +41,7 @@ func createDaemonWithJetStream(t *testing.T) *Daemon { natsURL := ns.ClientURL() - tmpDir, err := os.MkdirTemp("", "spx-state-test-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + tmpDir := t.TempDir() clusterCfg := &config.ClusterConfig{ Node: "node-1", @@ -260,7 +255,7 @@ func TestEveryState_CanReachTerminated(t *testing.T) { continue } - visited := map[vm.InstanceState]bool{start: true} + visited := map[vm.InstanceState]bool{start: true} //nolint:exhaustive // BFS seed — populated dynamically queue := []vm.InstanceState{start} found := false @@ -406,7 +401,7 @@ func TestTransitionState_ConcurrentTransitions(t *testing.T) { // reachableStates returns all states reachable from start via BFS. func reachableStates(start vm.InstanceState) map[vm.InstanceState]bool { - visited := map[vm.InstanceState]bool{start: true} + visited := map[vm.InstanceState]bool{start: true} //nolint:exhaustive // BFS seed — populated dynamically queue := []vm.InstanceState{start} for len(queue) > 0 { current := queue[0] diff --git a/spinifex/gateway/account.go b/spinifex/gateway/account.go index 05e9e440a..fa18572e9 100644 --- a/spinifex/gateway/account.go +++ b/spinifex/gateway/account.go @@ -3,6 +3,5 @@ package gateway import "net/http" func (gw *GatewayConfig) Account_Request(w http.ResponseWriter, r *http.Request) error { - return nil } diff --git a/spinifex/gateway/auth.go b/spinifex/gateway/auth.go index f792533dd..c11600a1c 100644 --- a/spinifex/gateway/auth.go +++ b/spinifex/gateway/auth.go @@ -28,7 +28,7 @@ func (gw *GatewayConfig) SigV4AuthMiddleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Skip OPTIONS requests (CORS preflight) - if r.Method == "OPTIONS" { + if r.Method == http.MethodOptions { next.ServeHTTP(w, r) return } @@ -301,7 +301,7 @@ func canonicalHeaderName(header string) string { // writeSigV4Error writes an EC2-compatible XML error response for authentication failures. func (gw *GatewayConfig) writeSigV4Error(w http.ResponseWriter, r *http.Request, errorCode string) { - requestID := r.Header.Get("x-amz-request-id") + requestID := r.Header.Get("X-Amz-Request-Id") if requestID == "" { requestID = uuid.NewString() } diff --git a/spinifex/gateway/auth_test.go b/spinifex/gateway/auth_test.go index 80e367058..2901db077 100644 --- a/spinifex/gateway/auth_test.go +++ b/spinifex/gateway/auth_test.go @@ -211,12 +211,12 @@ func setupTestApp(accessKey, secretKey string) http.Handler { func TestSigV4Auth_NoAuthorizationHeader(t *testing.T) { handler := setupTestApp(testAccessKey, testSecretKey) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" resp := doRequest(handler, req) - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { t.Errorf("Expected status 403, got %d", resp.StatusCode) } @@ -242,14 +242,14 @@ func TestSigV4Auth_MalformedHeader(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", tc.authHeader) req.Header.Set("X-Amz-Date", time.Now().UTC().Format(auth.TimeFormat)) resp := doRequest(handler, req) - if resp.StatusCode != 400 { + if resp.StatusCode != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", resp.StatusCode) } @@ -269,14 +269,14 @@ func TestSigV4Auth_InvalidAccessKey(t *testing.T) { "INVALID_ACCESS_KEY", testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { t.Errorf("Expected status 403, got %d", resp.StatusCode) } @@ -295,14 +295,14 @@ func TestSigV4Auth_InvalidSignature(t *testing.T) { testAccessKey, "WRONG_SECRET_KEY", testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { t.Errorf("Expected status 403, got %d", resp.StatusCode) } @@ -320,14 +320,14 @@ func TestSigV4Auth_ValidSignature(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) } @@ -336,13 +336,13 @@ func TestSigV4Auth_ValidSignature(t *testing.T) { func TestSigV4Auth_OptionsSkipsAuth(t *testing.T) { handler := setupTestApp(testAccessKey, testSecretKey) - req := httptest.NewRequest("OPTIONS", "/", nil) + req := httptest.NewRequest(http.MethodOptions, "/", nil) req.Host = "localhost:9999" // No Authorization header resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { t.Errorf("Expected status 200 for OPTIONS, got %d", resp.StatusCode) } } @@ -356,7 +356,7 @@ func TestSigV4Auth_ValidSignatureWithBody(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) @@ -364,7 +364,7 @@ func TestSigV4Auth_ValidSignatureWithBody(t *testing.T) { resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(respBody)) } @@ -379,14 +379,14 @@ func TestSigV4Auth_ValidSignatureWithQueryString(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/?"+queryString, nil) + req := httptest.NewRequest(http.MethodGet, "/?"+queryString, nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(respBody)) } @@ -427,14 +427,14 @@ func TestSigV4Auth_InactiveAccessKey(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(r, req) - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { t.Errorf("Expected status 403 for inactive key, got %d", resp.StatusCode) } @@ -462,14 +462,14 @@ func TestSigV4Auth_NilIAMService(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(r, req) - if resp.StatusCode != 500 { + if resp.StatusCode != http.StatusInternalServerError { t.Errorf("Expected status 500 for nil IAM service, got %d", resp.StatusCode) } @@ -515,14 +515,14 @@ func TestSigV4Auth_CallerIdentity(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(r, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Fatalf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) } @@ -650,14 +650,14 @@ func TestSigV4Auth_DecryptFailure(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(r, req) - if resp.StatusCode != 500 { + if resp.StatusCode != http.StatusInternalServerError { t.Errorf("Expected status 500 for decrypt failure, got %d", resp.StatusCode) } @@ -786,14 +786,14 @@ func TestSigV4Auth_ExpiredTimestamp(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, past, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { t.Errorf("Expected status 403, got %d", resp.StatusCode) } @@ -813,14 +813,14 @@ func TestSigV4Auth_FutureTimestamp(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, future, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { t.Errorf("Expected status 403, got %d", resp.StatusCode) } @@ -840,14 +840,14 @@ func TestSigV4Auth_TimestampWithinSkew(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, recent, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) } @@ -878,18 +878,18 @@ func TestSigV4Auth_ClockSkewBoundary(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, at, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if tc.expectPass && resp.StatusCode != 200 { + if tc.expectPass && resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected 200, got %d, body: %s", resp.StatusCode, string(body)) } - if !tc.expectPass && resp.StatusCode != 403 { + if !tc.expectPass && resp.StatusCode != http.StatusForbidden { t.Errorf("Expected 403, got %d", resp.StatusCode) } }) @@ -904,14 +904,14 @@ func TestSigV4Auth_MissingXAmzDate(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) // Deliberately omit X-Amz-Date resp := doRequest(handler, req) - if resp.StatusCode != 400 { + if resp.StatusCode != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", resp.StatusCode) } @@ -929,14 +929,14 @@ func TestSigV4Auth_MalformedTimestamp(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", "not-a-valid-date") resp := doRequest(handler, req) - if resp.StatusCode != 400 { + if resp.StatusCode != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", resp.StatusCode) } @@ -965,7 +965,7 @@ func TestWriteSigV4Error_ResponseFormat(t *testing.T) { for _, tc := range testCases { t.Run(tc.errorCode, func(t *testing.T) { w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) gw.writeSigV4Error(w, req, tc.errorCode) resp := w.Result() @@ -1002,8 +1002,8 @@ func TestWriteSigV4Error_PreservesRequestID(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, Region: testRegion} w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("x-amz-request-id", "test-request-id-123") + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("X-Amz-Request-Id", "test-request-id-123") gw.writeSigV4Error(w, req, awserrors.ErrorIncompleteSignature) @@ -1059,14 +1059,14 @@ func TestSigV4Auth_ContextPropagation(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(r, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Fatalf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1142,7 +1142,7 @@ func TestSigV4Auth_ContextDoesNotLeakBetweenRequests(t *testing.T) { "GET", "/", "", "", testAccessKey, testSecretKey, testRegion, testService, ) - req1 := httptest.NewRequest("GET", "/", nil) + req1 := httptest.NewRequest(http.MethodGet, "/", nil) req1.Host = "localhost:9999" req1.Header.Set("Authorization", authHeader1) req1.Header.Set("X-Amz-Date", timestamp1) @@ -1158,7 +1158,7 @@ func TestSigV4Auth_ContextDoesNotLeakBetweenRequests(t *testing.T) { "GET", "/", "", "", secondKey, testSecretKey, testRegion, testService, ) - req2 := httptest.NewRequest("GET", "/", nil) + req2 := httptest.NewRequest(http.MethodGet, "/", nil) req2.Host = "localhost:9999" req2.Header.Set("Authorization", authHeader2) req2.Header.Set("X-Amz-Date", timestamp2) @@ -1191,14 +1191,14 @@ func TestSigV4Auth_LookupAccessKeyUnexpectedError(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(r, req) - if resp.StatusCode != 500 { + if resp.StatusCode != http.StatusInternalServerError { t.Errorf("Expected status 500 for unexpected lookup error, got %d", resp.StatusCode) } @@ -1230,14 +1230,14 @@ func TestSigV4Auth_PathWithSpecialCharacters(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", tc.reqPath, nil) + req := httptest.NewRequest(http.MethodGet, tc.reqPath, nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1291,7 +1291,7 @@ func TestSigV4Auth_SignedContentType(t *testing.T) { headers, "content-type;host;x-amz-date", ) - req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) @@ -1299,7 +1299,7 @@ func TestSigV4Auth_SignedContentType(t *testing.T) { resp := doRequest(r, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(respBody)) } @@ -1314,14 +1314,14 @@ func TestSigV4Auth_EmptyBodyPOST(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("POST", "/", strings.NewReader("")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("")) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1388,7 +1388,7 @@ func TestComputeSignatureWithSecret_SpecialURIPaths(t *testing.T) { if reqPath == "" { reqPath = "/" } - req := httptest.NewRequest("GET", reqPath, nil) + req := httptest.NewRequest(http.MethodGet, reqPath, nil) req.Host = "localhost:9999" req.Header.Set("X-Amz-Date", timestamp) @@ -1412,7 +1412,7 @@ func TestComputeSignatureWithSecret_Determinism(t *testing.T) { var first string for i := range 10 { - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) req.Host = "localhost:9999" req.Header.Set("X-Amz-Date", timestamp) @@ -1442,7 +1442,7 @@ func TestComputeSignatureWithSecret_MultipartContentType(t *testing.T) { headers, "content-type;host;x-amz-date", ) - req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) @@ -1450,7 +1450,7 @@ func TestComputeSignatureWithSecret_MultipartContentType(t *testing.T) { resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200 for multipart content-type, got %d, body: %s", resp.StatusCode, string(respBody)) } @@ -1475,14 +1475,14 @@ func TestSigV4Auth_QueryStringEdgeCases(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/?"+tc.queryString, nil) + req := httptest.NewRequest(http.MethodGet, "/?"+tc.queryString, nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1495,6 +1495,7 @@ func TestSigV4Auth_QueryStringEdgeCases(t *testing.T) { // policyMockIAMService extends mockIAMService with configurable GetUserPolicies. type policyMockIAMService struct { mockIAMService + getUserPoliciesFn func(accountID, userName string) ([]handlers_iam.PolicyDocument, error) } @@ -1508,6 +1509,7 @@ func (m *policyMockIAMService) GetUserPolicies(accountID, userName string) ([]ha // errorLookupMockIAMService returns a configurable error from LookupAccessKey. type errorLookupMockIAMService struct { mockIAMService + err error } @@ -1566,14 +1568,14 @@ func TestCheckPolicy_NonRootNoPolicies_Denied(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 403, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1632,14 +1634,14 @@ func TestCheckPolicy_NonRootWithAllow_Passes(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1698,14 +1700,14 @@ func TestCheckPolicy_NonRootWithExplicitDeny_Denied(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 403, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1765,14 +1767,14 @@ func TestCheckPolicy_RootGlobalAccount_Bypasses(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200 (root bypass), got %d, body: %s", resp.StatusCode, string(body)) } @@ -1813,14 +1815,14 @@ func TestCheckPolicy_MissingAccountID_InternalError(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 500 { + if resp.StatusCode != http.StatusInternalServerError { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 500, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1863,14 +1865,14 @@ func TestCheckPolicy_GetUserPoliciesError_InternalError(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) resp := doRequest(handler, req) - if resp.StatusCode != 500 { + if resp.StatusCode != http.StatusInternalServerError { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 500, got %d, body: %s", resp.StatusCode, string(body)) } @@ -1915,7 +1917,7 @@ func TestCheckPolicy_RootNonGlobalAccount_StillEvaluated(t *testing.T) { testAccessKey, testSecretKey, testRegion, testService, ) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" req.Header.Set("Authorization", authHeader) req.Header.Set("X-Amz-Date", timestamp) @@ -1924,7 +1926,7 @@ func TestCheckPolicy_RootNonGlobalAccount_StillEvaluated(t *testing.T) { // root on non-global account is evaluated by the policy engine like any // other user. With no policies attached, the default deny applies. - if resp.StatusCode != 403 { + if resp.StatusCode != http.StatusForbidden { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 403, got %d, body: %s", resp.StatusCode, string(body)) } diff --git a/spinifex/gateway/ec2/image/DescribeImages.go b/spinifex/gateway/ec2/image/DescribeImages.go index d90039fb3..db91e6f50 100644 --- a/spinifex/gateway/ec2/image/DescribeImages.go +++ b/spinifex/gateway/ec2/image/DescribeImages.go @@ -24,11 +24,10 @@ func ValidateDescribeImagesInput(input *ec2.DescribeImagesInput) (err error) { } } - return + return err } func DescribeImages(input *ec2.DescribeImagesInput, natsConn *nats.Conn, accountID string) (output ec2.DescribeImagesOutput, err error) { - // Validate input err = ValidateDescribeImagesInput(input) diff --git a/spinifex/gateway/ec2/instance/RebootInstances.go b/spinifex/gateway/ec2/instance/RebootInstances.go index 3bd204c0b..cae94eeb1 100644 --- a/spinifex/gateway/ec2/instance/RebootInstances.go +++ b/spinifex/gateway/ec2/instance/RebootInstances.go @@ -57,7 +57,10 @@ func RebootInstances(input *ec2.RebootInstancesInput, natsConn *nats.Conn, accou describeInput := &ec2.DescribeInstancesInput{ InstanceIds: []*string{&instanceID}, } - describeData, _ := json.Marshal(describeInput) + describeData, err := json.Marshal(describeInput) + if err != nil { + return nil, fmt.Errorf("marshal describe input: %w", err) + } if reservations := queryInstanceBucket(natsConn, "ec2.DescribeStoppedInstances", describeData, accountID); len(reservations) > 0 { return nil, errors.New(awserrors.ErrorIncorrectInstanceState) } diff --git a/spinifex/gateway/ec2/instance/RunInstances.go b/spinifex/gateway/ec2/instance/RunInstances.go index 4c062b761..d14833fcf 100644 --- a/spinifex/gateway/ec2/instance/RunInstances.go +++ b/spinifex/gateway/ec2/instance/RunInstances.go @@ -53,15 +53,12 @@ func ValidateRunInstancesInput(input *ec2.RunInstancesInput) (err error) { if !strings.HasPrefix(*input.ImageId, "ami-") { return errors.New(awserrors.ErrorInvalidAMIIDMalformed) - } - return - + return err } func RunInstances(input *ec2.RunInstancesInput, natsConn *nats.Conn, accountID string) (reservation ec2.Reservation, err error) { - // Validate input err = ValidateRunInstancesInput(input) diff --git a/spinifex/gateway/ec2/instance/RunInstances_test.go b/spinifex/gateway/ec2/instance/RunInstances_test.go index 29b645894..632216f95 100644 --- a/spinifex/gateway/ec2/instance/RunInstances_test.go +++ b/spinifex/gateway/ec2/instance/RunInstances_test.go @@ -23,7 +23,6 @@ var defaults = ec2.RunInstancesInput{ } func TestParseRunInstances(t *testing.T) { - // Group multiple tests tests := []struct { name string @@ -214,5 +213,4 @@ func TestParseRunInstances(t *testing.T) { } // Additional test - } diff --git a/spinifex/gateway/ec2/key/CreateKeyPair.go b/spinifex/gateway/ec2/key/CreateKeyPair.go index 447c22ab1..6277f8c54 100644 --- a/spinifex/gateway/ec2/key/CreateKeyPair.go +++ b/spinifex/gateway/ec2/key/CreateKeyPair.go @@ -19,11 +19,10 @@ func ValidateCreateKeyPairInput(input *ec2.CreateKeyPairInput) (err error) { return errors.New(awserrors.ErrorMissingParameter) } - return + return err } func CreateKeyPair(input *ec2.CreateKeyPairInput, natsConn *nats.Conn, accountID string) (output ec2.CreateKeyPairOutput, err error) { - // Validate input err = ValidateCreateKeyPairInput(input) diff --git a/spinifex/gateway/ec2/key/DeleteKeyPair.go b/spinifex/gateway/ec2/key/DeleteKeyPair.go index 301f5f3d5..6657555ea 100644 --- a/spinifex/gateway/ec2/key/DeleteKeyPair.go +++ b/spinifex/gateway/ec2/key/DeleteKeyPair.go @@ -20,11 +20,10 @@ func ValidateDeleteKeyPairInput(input *ec2.DeleteKeyPairInput) (err error) { return errors.New(awserrors.ErrorMissingParameter) } - return + return err } func DeleteKeyPair(input *ec2.DeleteKeyPairInput, natsConn *nats.Conn, accountID string) (output ec2.DeleteKeyPairOutput, err error) { - // Validate input err = ValidateDeleteKeyPairInput(input) diff --git a/spinifex/gateway/ec2/key/ImportKeyPair.go b/spinifex/gateway/ec2/key/ImportKeyPair.go index 2b0054b31..83d82abdf 100644 --- a/spinifex/gateway/ec2/key/ImportKeyPair.go +++ b/spinifex/gateway/ec2/key/ImportKeyPair.go @@ -22,7 +22,7 @@ func ValidateImportKeyPairInput(input *ec2.ImportKeyPairInput) (err error) { return errors.New(awserrors.ErrorMissingParameter) } - return + return err } func ImportKeyPair(input *ec2.ImportKeyPairInput, natsConn *nats.Conn, accountID string) (output ec2.ImportKeyPairOutput, err error) { diff --git a/spinifex/gateway/ec2/volume/DetachVolume.go b/spinifex/gateway/ec2/volume/DetachVolume.go index 611092284..1c02b9358 100644 --- a/spinifex/gateway/ec2/volume/DetachVolume.go +++ b/spinifex/gateway/ec2/volume/DetachVolume.go @@ -39,7 +39,7 @@ func DetachVolume(input *ec2.DetachVolumeInput, natsConn *nats.Conn, accountID s volumeID := *input.VolumeId // Resolve InstanceId if not provided by looking up volume metadata - instanceID := "" + var instanceID string if input.InstanceId != nil && *input.InstanceId != "" { instanceID = *input.InstanceId } else { diff --git a/spinifex/gateway/ec2/zone/AvailabilityZone.go b/spinifex/gateway/ec2/zone/AvailabilityZone.go index d58ddd851..905be7bac 100644 --- a/spinifex/gateway/ec2/zone/AvailabilityZone.go +++ b/spinifex/gateway/ec2/zone/AvailabilityZone.go @@ -6,7 +6,6 @@ import ( ) func DescribeAvailabilityZones(input *ec2.DescribeAvailabilityZonesInput, region string, az string) (output *ec2.DescribeAvailabilityZonesOutput, err error) { - output = &ec2.DescribeAvailabilityZonesOutput{ AvailabilityZones: []*ec2.AvailabilityZone{ { @@ -27,7 +26,6 @@ func DescribeAvailabilityZones(input *ec2.DescribeAvailabilityZonesInput, region } func DescribeRegions(input *ec2.DescribeRegionsInput, region string) (output *ec2.DescribeRegionsOutput, err error) { - output = &ec2.DescribeRegionsOutput{ Regions: []*ec2.Region{ { diff --git a/spinifex/gateway/gateway.go b/spinifex/gateway/gateway.go index e8e224371..1067e266e 100644 --- a/spinifex/gateway/gateway.go +++ b/spinifex/gateway/gateway.go @@ -68,7 +68,6 @@ type ErrorDetail struct { } func (gw *GatewayConfig) SetupRoutes() http.Handler { - var logLevel slog.Level if gw.Debug { @@ -159,7 +158,7 @@ func corsMiddleware(next http.Handler) http.Handler { w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,HEAD,OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "*") w.Header().Set("Access-Control-Allow-Credentials", "true") - if r.Method == "OPTIONS" { + if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } @@ -173,7 +172,6 @@ func corsMiddleware(next http.Handler) http.Handler { // AWS_ENDPOINT_URL=https://localhost:9999/ aws --no-verify-ssl ec2 describe-instances func (gw *GatewayConfig) Request(w http.ResponseWriter, r *http.Request) { - // Route the request to the appropriate endpoint (e.g EC2, IAM, etc) svc, err := gw.GetService(r) slog.Info("Request", "service", svc, "method", r.Method, "path", r.URL.Path) @@ -291,7 +289,7 @@ func (gw *GatewayConfig) ErrorHandler(w http.ResponseWriter, r *http.Request, er // Get the request ID var requestId = uuid.NewString() - if rid := r.Header.Get("x-amz-request-id"); rid != "" { + if rid := r.Header.Get("X-Amz-Request-Id"); rid != "" { requestId = rid } @@ -346,7 +344,6 @@ func ParseAWSQueryArgs(query string) map[string]string { } func GenerateEC2ErrorResponse(code, message, requestID string) (output []byte) { - errorXml := ErrorResponse{ Errors: Errors{ Error: ErrorDetail{ @@ -405,7 +402,6 @@ func GenerateIAMErrorResponse(code, message, requestID string) (output []byte) { } func ParseArgsToStruct(input *any, args map[string]string) (err error) { - // Generated from input shape: RunInstancesRequest err = awsec2query.QueryParamsToStruct(args, input) @@ -414,7 +410,6 @@ func ParseArgsToStruct(input *any, args map[string]string) (err error) { } return nil - } // DiscoverActiveNodes discovers the number of active spinifex daemon nodes in the cluster @@ -487,6 +482,7 @@ func (gw *GatewayConfig) DiscoverActiveNodes() int { // statusWriter wraps http.ResponseWriter to capture the status code. type statusWriter struct { http.ResponseWriter + status int } diff --git a/spinifex/gateway/gateway_test.go b/spinifex/gateway/gateway_test.go index 8d8b3f848..997e0526e 100644 --- a/spinifex/gateway/gateway_test.go +++ b/spinifex/gateway/gateway_test.go @@ -175,7 +175,7 @@ func TestErrorHandler_IAMService(t *testing.T) { gw.ErrorHandler(w, r, errors.New(awserrors.ErrorIAMNoSuchEntity)) }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) resp := doRequest(handler, req) assert.Equal(t, 404, resp.StatusCode) @@ -197,7 +197,7 @@ func TestErrorHandler_UnknownError(t *testing.T) { gw.ErrorHandler(w, r, errors.New("SomeCompletelyBogusError")) }) - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) resp := doRequest(handler, req) assert.Equal(t, 500, resp.StatusCode) @@ -218,7 +218,7 @@ func TestErrorHandler_EC2Service(t *testing.T) { gw.ErrorHandler(w, r, errors.New(awserrors.ErrorInvalidParameterValue)) }) - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) resp := doRequest(handler, req) assert.Equal(t, 400, resp.StatusCode) @@ -240,8 +240,8 @@ func TestErrorHandler_UsesRequestIDHeader(t *testing.T) { gw.ErrorHandler(w, r, errors.New(awserrors.ErrorInternalError)) }) - req := httptest.NewRequest("POST", "/", nil) - req.Header.Set("x-amz-request-id", "custom-req-id-123") + req := httptest.NewRequest(http.MethodPost, "/", nil) + req.Header.Set("X-Amz-Request-Id", "custom-req-id-123") resp := doRequest(handler, req) body, _ := io.ReadAll(resp.Body) @@ -257,7 +257,7 @@ func TestErrorHandler_ContentTypeXML(t *testing.T) { gw.ErrorHandler(w, r, errors.New(awserrors.ErrorInternalError)) }) - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) resp := doRequest(handler, req) assert.Equal(t, "application/xml", resp.Header.Get("Content-Type")) } @@ -493,7 +493,7 @@ func TestGetService(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) if tc.ctxVal != nil { ctx := context.WithValue(req.Context(), ctxService, tc.ctxVal) req = req.WithContext(ctx) @@ -514,7 +514,7 @@ func TestGetService(t *testing.T) { func TestRequest_NoServiceContext(t *testing.T) { gw := &GatewayConfig{DisableLogging: true} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) w := httptest.NewRecorder() gw.Request(w, req) @@ -527,7 +527,7 @@ func TestRequest_NoServiceContext(t *testing.T) { func TestRequest_UnsupportedService(t *testing.T) { gw := &GatewayConfig{DisableLogging: true} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxService, "s3") req = req.WithContext(ctx) w := httptest.NewRecorder() @@ -542,7 +542,7 @@ func TestRequest_UnsupportedService(t *testing.T) { func TestRequest_EC2MissingAction(t *testing.T) { gw := &GatewayConfig{DisableLogging: true} - req := httptest.NewRequest("POST", "/", strings.NewReader("")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("")) ctx := context.WithValue(req.Context(), ctxService, "ec2") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -558,7 +558,7 @@ func TestRequest_EC2MissingAction(t *testing.T) { func TestRequest_IAMNilService(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: nil} - req := httptest.NewRequest("POST", "/", strings.NewReader("Action=CreateUser&UserName=test")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=CreateUser&UserName=test")) ctx := context.WithValue(req.Context(), ctxService, "iam") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -574,7 +574,7 @@ func TestRequest_IAMNilService(t *testing.T) { func TestRequest_AccountReturns200(t *testing.T) { gw := &GatewayConfig{DisableLogging: true} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxService, "account") req = req.WithContext(ctx) w := httptest.NewRecorder() @@ -587,7 +587,7 @@ func TestRequest_AccountReturns200(t *testing.T) { // setupEC2Request creates an http.Request with EC2 service context and optional account ID. func setupEC2Request(body string, accountID string) *http.Request { - req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) ctx := context.WithValue(req.Context(), ctxService, "ec2") if accountID != "" { ctx = context.WithValue(ctx, ctxAccountID, accountID) @@ -697,7 +697,7 @@ func TestEC2Request_DescribeAvailabilityZones(t *testing.T) { func TestCheckPolicy_NilIAMService(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: nil} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) err := gw.checkPolicy(req, "ec2", "DescribeInstances") assert.NoError(t, err) @@ -705,7 +705,7 @@ func TestCheckPolicy_NilIAMService(t *testing.T) { func TestCheckPolicy_NoIdentityInContext(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) err := gw.checkPolicy(req, "ec2", "DescribeInstances") assert.NoError(t, err) @@ -713,7 +713,7 @@ func TestCheckPolicy_NoIdentityInContext(t *testing.T) { func TestCheckPolicy_RootUserGlobalAccount(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "root") ctx = context.WithValue(ctx, ctxAccountID, "000000000000") // GlobalAccountID req = req.WithContext(ctx) @@ -736,7 +736,7 @@ func TestCheckPolicy_NonRootAllowPolicy(t *testing.T) { }, } gw := &GatewayConfig{DisableLogging: true, IAMService: mock} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "alice") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -759,7 +759,7 @@ func TestCheckPolicy_NonRootDenyPolicy(t *testing.T) { }, } gw := &GatewayConfig{DisableLogging: true, IAMService: mock} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "alice") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -776,7 +776,7 @@ func TestCheckPolicy_NonRootNoPolicies(t *testing.T) { }, } gw := &GatewayConfig{DisableLogging: true, IAMService: mock} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "alice") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -793,7 +793,7 @@ func TestCheckPolicy_GetUserPoliciesError(t *testing.T) { }, } gw := &GatewayConfig{DisableLogging: true, IAMService: mock} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "alice") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -805,7 +805,7 @@ func TestCheckPolicy_GetUserPoliciesError(t *testing.T) { func TestCheckPolicy_EmptyIdentity(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -816,7 +816,7 @@ func TestCheckPolicy_EmptyIdentity(t *testing.T) { func TestCheckPolicy_MissingAccountID(t *testing.T) { gw := &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "alice") // No account ID req = req.WithContext(ctx) @@ -835,7 +835,7 @@ func TestCheckPolicy_NATSTransientRetriesAllAttempts(t *testing.T) { }, } gw := &GatewayConfig{DisableLogging: true, IAMService: mock} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "alice") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -865,7 +865,7 @@ func TestCheckPolicy_NATSTransientRetriesThenSucceeds(t *testing.T) { }, } gw := &GatewayConfig{DisableLogging: true, IAMService: mock} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "alice") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -882,7 +882,7 @@ func TestCheckPolicy_NonTransientErrorStillFails(t *testing.T) { }, } gw := &GatewayConfig{DisableLogging: true, IAMService: mock} - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ctx := context.WithValue(req.Context(), ctxIdentity, "alice") ctx = context.WithValue(ctx, ctxAccountID, "123456789012") req = req.WithContext(ctx) @@ -904,11 +904,11 @@ func TestCorsMiddleware_GETRequest(t *testing.T) { nextCalled := false next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { nextCalled = true - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) }) handler := corsMiddleware(next) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) @@ -926,7 +926,7 @@ func TestCorsMiddleware_OPTIONSRequest(t *testing.T) { }) handler := corsMiddleware(next) - req := httptest.NewRequest("OPTIONS", "/", nil) + req := httptest.NewRequest(http.MethodOptions, "/", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) @@ -937,7 +937,7 @@ func TestCorsMiddleware_OPTIONSRequest(t *testing.T) { func TestCorsMiddleware_LocalIPOrigin(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) }) handler := corsMiddleware(next) @@ -948,7 +948,7 @@ func TestCorsMiddleware_LocalIPOrigin(t *testing.T) { if origin == "https://localhost:3000" { continue } - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Origin", origin) w := httptest.NewRecorder() handler.ServeHTTP(w, req) @@ -959,11 +959,11 @@ func TestCorsMiddleware_LocalIPOrigin(t *testing.T) { func TestCorsMiddleware_UnknownOrigin(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) }) handler := corsMiddleware(next) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Origin", "https://evil.example.com") w := httptest.NewRecorder() handler.ServeHTTP(w, req) @@ -979,7 +979,7 @@ func TestSlogRequestLogger_CallsNext(t *testing.T) { }) handler := slogRequestLogger(next) - req := httptest.NewRequest("GET", "/test", nil) + req := httptest.NewRequest(http.MethodGet, "/test", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) @@ -993,7 +993,7 @@ func TestSlogRequestLogger_CapturesStatusCode(t *testing.T) { }) handler := slogRequestLogger(next) - req := httptest.NewRequest("GET", "/missing", nil) + req := httptest.NewRequest(http.MethodGet, "/missing", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) diff --git a/spinifex/gateway/iam_request_test.go b/spinifex/gateway/iam_request_test.go index 92a24c981..56118d239 100644 --- a/spinifex/gateway/iam_request_test.go +++ b/spinifex/gateway/iam_request_test.go @@ -167,7 +167,7 @@ func TestIAMRequest_CreateUser_Success(t *testing.T) { } handler := setupIAMRequestHandler(svc) - req := httptest.NewRequest("POST", "/", strings.NewReader("Action=CreateUser&UserName=alice")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=CreateUser&UserName=alice")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := doRequest(handler, req) @@ -193,7 +193,7 @@ func TestIAMRequest_ListUsers_Success(t *testing.T) { } handler := setupIAMRequestHandler(svc) - req := httptest.NewRequest("POST", "/", strings.NewReader("Action=ListUsers")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=ListUsers")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := doRequest(handler, req) @@ -209,7 +209,7 @@ func TestIAMRequest_ListUsers_Success(t *testing.T) { func TestIAMRequest_UnknownAction(t *testing.T) { handler := setupIAMRequestHandler(&flexMockIAMService{}) - req := httptest.NewRequest("POST", "/", strings.NewReader("Action=DoesNotExist")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=DoesNotExist")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := doRequest(handler, req) @@ -222,7 +222,7 @@ func TestIAMRequest_UnknownAction(t *testing.T) { func TestIAMRequest_EmptyAction(t *testing.T) { handler := setupIAMRequestHandler(&flexMockIAMService{}) - req := httptest.NewRequest("POST", "/", strings.NewReader("UserName=alice")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("UserName=alice")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := doRequest(handler, req) @@ -247,7 +247,7 @@ func TestIAMRequest_NilService(t *testing.T) { } }) - req := httptest.NewRequest("POST", "/", strings.NewReader("Action=CreateUser&UserName=alice")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=CreateUser&UserName=alice")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := doRequest(handler, req) @@ -265,7 +265,7 @@ func TestIAMRequest_ServiceError(t *testing.T) { } handler := setupIAMRequestHandler(svc) - req := httptest.NewRequest("POST", "/", strings.NewReader("Action=CreateUser&UserName=alice")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=CreateUser&UserName=alice")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := doRequest(handler, req) @@ -291,7 +291,7 @@ func TestIAMRequest_MissingAccountID(t *testing.T) { } }) - req := httptest.NewRequest("POST", "/", strings.NewReader("Action=CreateUser&UserName=alice")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=CreateUser&UserName=alice")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := doRequest(handler, req) @@ -305,7 +305,7 @@ func TestIAMRequest_ValidationError(t *testing.T) { // CreateUser with missing UserName should return MissingParameter handler := setupIAMRequestHandler(&flexMockIAMService{}) - req := httptest.NewRequest("POST", "/", strings.NewReader("Action=CreateUser")) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=CreateUser")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := doRequest(handler, req) diff --git a/spinifex/gateway/policy/evaluator.go b/spinifex/gateway/policy/evaluator.go index 2e78421e9..a8e8f1160 100644 --- a/spinifex/gateway/policy/evaluator.go +++ b/spinifex/gateway/policy/evaluator.go @@ -31,7 +31,7 @@ func EvaluateAccess(identity, action, resource string, policies []handlers_iam.P hasAllow := false for i := range policies { for j := range policies[i].Statement { - stmt := &policies[i].Statement[j] + stmt := &policies[i].Statement[j] //nolint:gosec // G602 false positive: j is bounded by range if !matchesAny(stmt.Action, action) { continue diff --git a/spinifex/handlers/ec2/igw/service_impl.go b/spinifex/handlers/ec2/igw/service_impl.go index 76f205e9f..7e58534f5 100644 --- a/spinifex/handlers/ec2/igw/service_impl.go +++ b/spinifex/handlers/ec2/igw/service_impl.go @@ -259,8 +259,10 @@ func (s *IGWServiceImpl) AttachInternetGateway(input *ec2.AttachInternetGatewayI InternetGatewayId: igwID, VpcId: vpcID, } - eventData, _ := json.Marshal(event) - if err := s.natsConn.Publish("vpc.igw-attach", eventData); err != nil { + eventData, err := json.Marshal(event) + if err != nil { + slog.Warn("Failed to marshal IGW attach event", "error", err) + } else if err := s.natsConn.Publish("vpc.igw-attach", eventData); err != nil { slog.Warn("Failed to publish IGW attach event", "error", err) } } @@ -315,8 +317,10 @@ func (s *IGWServiceImpl) DetachInternetGateway(input *ec2.DetachInternetGatewayI InternetGatewayId: igwID, VpcId: vpcID, } - eventData, _ := json.Marshal(event) - if err := s.natsConn.Publish("vpc.igw-detach", eventData); err != nil { + eventData, err := json.Marshal(event) + if err != nil { + slog.Warn("Failed to marshal IGW detach event", "error", err) + } else if err := s.natsConn.Publish("vpc.igw-detach", eventData); err != nil { slog.Warn("Failed to publish IGW detach event", "error", err) } } diff --git a/spinifex/handlers/ec2/instance/service_impl.go b/spinifex/handlers/ec2/instance/service_impl.go index f435f2fab..199a2acc1 100644 --- a/spinifex/handlers/ec2/instance/service_impl.go +++ b/spinifex/handlers/ec2/instance/service_impl.go @@ -297,7 +297,6 @@ func (s *InstanceServiceImpl) GenerateVolumes(input *ec2.RunInstancesInput, inst } return volumeInfos, nil - } // newViperblock creates a viperblock instance with the service's S3/Predastore credentials. diff --git a/spinifex/handlers/ec2/volume/service_impl.go b/spinifex/handlers/ec2/volume/service_impl.go index fa6a88ae3..c3cd41bc3 100644 --- a/spinifex/handlers/ec2/volume/service_impl.go +++ b/spinifex/handlers/ec2/volume/service_impl.go @@ -620,7 +620,7 @@ func (s *VolumeServiceImpl) mergeVolumeConfig(configKey string, cfg *viperblock. // Full VBState exists -- update VolumeConfig and reconcile VolumeSize state.VolumeConfig = *cfg - configSizeBytes := uint64(cfg.VolumeMetadata.SizeGiB) * 1024 * 1024 * 1024 + configSizeBytes := cfg.VolumeMetadata.SizeGiB * 1024 * 1024 * 1024 if configSizeBytes > 0 && configSizeBytes > state.VolumeSize { state.VolumeSize = configSizeBytes } diff --git a/spinifex/handlers/ec2/volume/service_impl_test.go b/spinifex/handlers/ec2/volume/service_impl_test.go index 97812f93a..eecb44265 100644 --- a/spinifex/handlers/ec2/volume/service_impl_test.go +++ b/spinifex/handlers/ec2/volume/service_impl_test.go @@ -569,7 +569,7 @@ func createVolumeInStoreWithVBState(t *testing.T, store *objectstore.MemoryObjec t.Helper() state := viperblock.VBState{ VolumeName: volumeID, - VolumeSize: uint64(meta.SizeGiB) * 1024 * 1024 * 1024, + VolumeSize: meta.SizeGiB * 1024 * 1024 * 1024, BlockSize: blockSize, SeqNum: seqNum, VolumeConfig: viperblock.VolumeConfig{ diff --git a/spinifex/handlers/ec2/vpc/service_impl.go b/spinifex/handlers/ec2/vpc/service_impl.go index 8350faffc..64ee019fa 100644 --- a/spinifex/handlers/ec2/vpc/service_impl.go +++ b/spinifex/handlers/ec2/vpc/service_impl.go @@ -134,7 +134,10 @@ func (s *VPCServiceImpl) nextVNI() (int64, error) { if errors.Is(err, nats.ErrKeyNotFound) { // First VNI allocation — initialize counter vni := int64(vniStart) - data, _ := json.Marshal(vni + 1) + data, marshalErr := json.Marshal(vni + 1) + if marshalErr != nil { + return 0, fmt.Errorf("failed to marshal VNI counter: %w", marshalErr) + } if _, err := s.vniKV.Create(vniCounterKey, data); err != nil { return 0, fmt.Errorf("failed to initialize VNI counter: %w", err) } @@ -149,7 +152,10 @@ func (s *VPCServiceImpl) nextVNI() (int64, error) { } next := current + 1 - data, _ := json.Marshal(next) + data, marshalErr := json.Marshal(next) + if marshalErr != nil { + return 0, fmt.Errorf("failed to marshal VNI counter: %w", marshalErr) + } if _, err := s.vniKV.Update(vniCounterKey, data, entry.Revision()); err != nil { return 0, fmt.Errorf("failed to update VNI counter (CAS conflict): %w", err) } diff --git a/spinifex/handlers/iam/service_impl_test.go b/spinifex/handlers/iam/service_impl_test.go index c86818231..364464ce4 100644 --- a/spinifex/handlers/iam/service_impl_test.go +++ b/spinifex/handlers/iam/service_impl_test.go @@ -2,7 +2,6 @@ package handlers_iam import ( "fmt" - "io" "log/slog" "strings" "testing" @@ -790,7 +789,7 @@ func TestSeedBootstrap_WithAdmin(t *testing.T) { policies, err := svc.GetUserPolicies("000000000001", "admin") require.NoError(t, err) require.Len(t, policies, 1) - assert.Equal(t, "Allow", string(policies[0].Statement[0].Effect)) + assert.Equal(t, "Allow", policies[0].Statement[0].Effect) assert.Equal(t, StringOrArr{"*"}, policies[0].Statement[0].Action) assert.Equal(t, StringOrArr{"*"}, policies[0].Statement[0].Resource) @@ -1556,7 +1555,7 @@ func TestSensitiveDataNotLogged_CreateAccessKey(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})) slog.SetDefault(logger) t.Cleanup(func() { - slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil))) + slog.SetDefault(slog.New(slog.DiscardHandler)) }) akOut, err := svc.CreateAccessKey(testAccountID, &iam.CreateAccessKeyInput{ @@ -1584,7 +1583,7 @@ func TestSensitiveDataNotLogged_MasterKey(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})) slog.SetDefault(logger) t.Cleanup(func() { - slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil))) + slog.SetDefault(slog.New(slog.DiscardHandler)) }) svc := setupTestIAMService(t) diff --git a/spinifex/service/service.go b/spinifex/service/service.go index 8b03ff6d0..64b71ea15 100644 --- a/spinifex/service/service.go +++ b/spinifex/service/service.go @@ -21,7 +21,6 @@ type Service interface { } func New(btype string, config any) (Service, error) { - switch btype { case "nats": return nats.New(config) @@ -43,7 +42,6 @@ func New(btype string, config any) (Service, error) { case "vpcd": return vpcd.New(config) - } return nil, fmt.Errorf("unknown service type: %s", btype) diff --git a/spinifex/service/service_test.go b/spinifex/service/service_test.go index 3ffa6d91f..9708d5b6e 100644 --- a/spinifex/service/service_test.go +++ b/spinifex/service/service_test.go @@ -15,12 +15,10 @@ func TestNew(t *testing.T) { services := []string{"nats", "predastore", "viperblock", "spinifex", "awsgw"} for _, s := range services { - var svc Service var err error switch s { - // TODO: Standardize service config handling (use config.Config for all?) case "nats": svc, err = New(s, &nats.Config{}) @@ -34,7 +32,6 @@ func TestNew(t *testing.T) { case "awsgw": svc, err = New(s, &config.ClusterConfig{}) // No special setup needed - } assert.NoError(t, err) diff --git a/spinifex/services/awsgw/awsgw.go b/spinifex/services/awsgw/awsgw.go index 3d428d358..828e64e64 100644 --- a/spinifex/services/awsgw/awsgw.go +++ b/spinifex/services/awsgw/awsgw.go @@ -70,7 +70,6 @@ func (svc *Service) Reload() (err error) { } func launchService(config *config.ClusterConfig) error { - nodeConfig := config.Nodes[config.Node] // Connect to NATS for service communication. On concurrent startup the diff --git a/spinifex/services/nats/nats.go b/spinifex/services/nats/nats.go index c0fc97343..476a388fa 100644 --- a/spinifex/services/nats/nats.go +++ b/spinifex/services/nats/nats.go @@ -72,20 +72,17 @@ func (svc *Service) Reload() (err error) { func launchService(config *Config) (err error) { // Create proper server options - opts := &server.Options{} + var opts *server.Options // If configFile set use, otherwise set defaults if config.ConfigFile != "" { - opts, err = server.ProcessConfigFile(config.ConfigFile) if err != nil { slog.Error("Failed to process NATS config file", "err", err) return err } - } else { - opts = &server.Options{ ConfigFile: config.ConfigFile, Port: config.Port, @@ -103,7 +100,6 @@ func launchService(config *Config) (err error) { if opts.Host == "" { opts.Host = "0.0.0.0" } - } slog.Debug("NATS server options", "opts", opts) @@ -127,5 +123,4 @@ func launchService(config *Config) (err error) { ns.WaitForShutdown() return nil - } diff --git a/spinifex/services/predastore/predastore_integration_test.go b/spinifex/services/predastore/predastore_integration_test.go index 4dc12677f..3409fd605 100644 --- a/spinifex/services/predastore/predastore_integration_test.go +++ b/spinifex/services/predastore/predastore_integration_test.go @@ -125,7 +125,7 @@ func startPredastoreServer(t *testing.T) *Config { } // Create persistent test environment (not using t.TempDir() for shared server) - testDir, err := os.MkdirTemp("", "predastore-integration-*") + testDir, err := os.MkdirTemp("", "predastore-integration-*") //nolint:usetesting // shared server outlives individual test if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } @@ -666,7 +666,7 @@ func TestIntegration_LargeFile(t *testing.T) { downloadedData, err := io.ReadAll(result.Body) result.Body.Close() assert.NoError(t, err, "Reading large file should succeed") - assert.Equal(t, size, len(downloadedData), "Downloaded size should match") + assert.Len(t, downloadedData, size, "Downloaded size should match") assert.Equal(t, largeData, downloadedData, "Downloaded content should match") // Cleanup diff --git a/spinifex/services/spinifex/spinifex.go b/spinifex/services/spinifex/spinifex.go index 51d0dca4b..0d2e360cd 100644 --- a/spinifex/services/spinifex/spinifex.go +++ b/spinifex/services/spinifex/spinifex.go @@ -82,5 +82,4 @@ func launchService(config *config.ClusterConfig, configPath string) (err error) } return nil - } diff --git a/spinifex/services/spinifexui/spinifexui.go b/spinifex/services/spinifexui/spinifexui.go index 5132de374..6df3419cf 100644 --- a/spinifex/services/spinifexui/spinifexui.go +++ b/spinifex/services/spinifexui/spinifexui.go @@ -314,6 +314,7 @@ var gzipContentTypes = map[string]bool{ // gzipResponseWriter wraps http.ResponseWriter to compress eligible responses. type gzipResponseWriter struct { http.ResponseWriter + gw *gzip.Writer wroteHeader bool compress bool @@ -370,6 +371,7 @@ const tlsRecordTypeHandshake = 0x16 // a plain-HTTP redirect to HTTPS is sent and the connection is closed. type tlsSplitListener struct { net.Listener + port int tlsCfg *tls.Config } @@ -445,6 +447,7 @@ func (ln *tlsSplitListener) redirectHTTP(conn net.Conn, firstByte byte) { // prefixConn wraps a net.Conn with a reader that replays prefixed bytes. type prefixConn struct { net.Conn + r io.Reader } diff --git a/spinifex/services/viperblockd/viperblockd.go b/spinifex/services/viperblockd/viperblockd.go index 75575845e..8208e54d5 100644 --- a/spinifex/services/viperblockd/viperblockd.go +++ b/spinifex/services/viperblockd/viperblockd.go @@ -158,7 +158,6 @@ func (svc *Service) Reload() (err error) { } func launchService(cfg *Config) (err error) { - // Connect to NATS nc, err := utils.ConnectNATS(cfg.NatsHost, cfg.NatsToken) if err != nil { @@ -268,7 +267,12 @@ func launchService(cfg *Config) (err error) { err := json.Unmarshal(msg.Data, &ebsRequest) if err != nil { slog.Error("Failed to unmarshal ebs.unmount message", "err", err) - errResp, _ := json.Marshal(types.EBSUnMountResponse{Error: fmt.Sprintf("bad request: %v", err)}) + errResp, marshalErr := json.Marshal(types.EBSUnMountResponse{Error: fmt.Sprintf("bad request: %v", err)}) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.unmount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(errResp); err != nil { slog.Error("Failed to respond to ebs.unmount request", "err", err) } @@ -279,7 +283,7 @@ func launchService(cfg *Config) (err error) { // then release before calling VB.Close() (which does heavy S3 I/O). var ebsResponse types.EBSUnMountResponse var matched MountedVolume - var matchIdx int = -1 + var matchIdx = -1 cfg.mu.Lock() for i, volume := range cfg.MountedVolumes { if volume.Name == ebsRequest.Name { @@ -335,6 +339,7 @@ func launchService(cfg *Config) (err error) { response, err := json.Marshal(ebsResponse) if err != nil { slog.Error("Failed to marshal response", "err", err) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) return } @@ -354,7 +359,12 @@ func launchService(cfg *Config) (err error) { var syncRequest types.EBSSyncRequest if err := json.Unmarshal(msg.Data, &syncRequest); err != nil { slog.Error("Failed to unmarshal ebs.sync message", "err", err) - errResp, _ := json.Marshal(types.EBSSyncResponse{Error: fmt.Sprintf("bad request: %v", err)}) + errResp, marshalErr := json.Marshal(types.EBSSyncResponse{Error: fmt.Sprintf("bad request: %v", err)}) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.sync error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(errResp); err != nil { slog.Error("Failed to respond to ebs.sync request", "err", err) } @@ -425,7 +435,12 @@ func launchService(cfg *Config) (err error) { err := json.Unmarshal(msg.Data, &ebsRequest) if err != nil { slog.Error("Failed to unmarshal ebs.mount message", "err", err) - errResp, _ := json.Marshal(types.EBSMountResponse{Error: fmt.Sprintf("bad request: %v", err)}) + errResp, marshalErr := json.Marshal(types.EBSMountResponse{Error: fmt.Sprintf("bad request: %v", err)}) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(errResp); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -484,7 +499,12 @@ func launchService(cfg *Config) (err error) { if err != nil { ebsResponse.Error = fmt.Sprintf("Failed to connect to Viperblock store: %v", err) // Marshal and send error response immediately - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -504,7 +524,12 @@ func launchService(cfg *Config) (err error) { if err != nil { ebsResponse.Error = err.Error() // Marshal and send error response immediately - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -521,7 +546,12 @@ func launchService(cfg *Config) (err error) { if err != nil { ebsResponse.Error = err.Error() // Marshal and send error response immediately - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -545,7 +575,12 @@ func launchService(cfg *Config) (err error) { portStr, err := viperblock.FindFreePort() if err != nil { ebsResponse.Error = err.Error() - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -561,7 +596,12 @@ func launchService(cfg *Config) (err error) { if err != nil { slog.Error("Failed to convert port to int", "err", err) ebsResponse.Error = fmt.Sprintf("failed to parse port: %v", err) - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -578,7 +618,12 @@ func launchService(cfg *Config) (err error) { nbdSocket, err = utils.GenerateUniqueSocketFile(ebsRequest.Name) if err != nil { ebsResponse.Error = err.Error() - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -597,7 +642,12 @@ func launchService(cfg *Config) (err error) { if err != nil { slog.Error("Failed to generate nbdkit pid file", "err", err) ebsResponse.Error = fmt.Sprintf("failed to generate pid file: %v", err) - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -667,7 +717,12 @@ func launchService(cfg *Config) (err error) { if pid == 0 { ebsResponse.Error = "Failed to start nbdkit" // Marshal and send error response immediately - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -685,7 +740,12 @@ func launchService(cfg *Config) (err error) { case exitErr := <-exitChan: if exitErr != 0 { ebsResponse.Error = fmt.Sprintf("nbdkit failed: %v", exitErr) - response, _ := json.Marshal(ebsResponse) + response, marshalErr := json.Marshal(ebsResponse) + if marshalErr != nil { + slog.Error("Failed to marshal ebs.mount error response", "err", marshalErr) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) + return + } if err := msg.Respond(response); err != nil { slog.Error("Failed to respond to ebs.mount request", "err", err) } @@ -724,6 +784,7 @@ func launchService(cfg *Config) (err error) { response, err := json.Marshal(ebsResponse) if err != nil { slog.Error("Failed to marshal response", "err", err) + _ = msg.Respond([]byte(`{"Error":"internal marshal failure"}`)) return } @@ -771,5 +832,4 @@ func launchService(cfg *Config) (err error) { } return nil - } diff --git a/spinifex/utils/account_test.go b/spinifex/utils/account_test.go index 2cf3ecd68..06156067c 100644 --- a/spinifex/utils/account_test.go +++ b/spinifex/utils/account_test.go @@ -1,7 +1,6 @@ package utils import ( - "os" "testing" "time" @@ -65,9 +64,7 @@ func TestGlobalAccountID(t *testing.T) { // startJSNATSServer starts an embedded JetStream-enabled NATS server for testing. func startJSNATSServer(t *testing.T) *server.Server { t.Helper() - tmpDir, err := os.MkdirTemp("", "nats-js-test-*") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + tmpDir := t.TempDir() ns, err := server.NewServer(&server.Options{ Host: "127.0.0.1", diff --git a/spinifex/utils/images.go b/spinifex/utils/images.go index 59bda781c..ece224b14 100644 --- a/spinifex/utils/images.go +++ b/spinifex/utils/images.go @@ -138,7 +138,6 @@ var AvailableImages = map[string]Images{ // AMI / image extraction utils func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string, err error) { - var args []string var execCmd string @@ -146,7 +145,7 @@ func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string _, err = os.Stat(imagepath) if err != nil { - return + return diskimage, err } // Extract the filepath @@ -154,7 +153,6 @@ func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string // Already in raw/image formt, confirm the file contains a valid disk image/MBR if strings.HasSuffix(imagefile, ".raw") || strings.HasSuffix(imagefile, ".img") || strings.HasSuffix(imagefile, ".qcow2") || strings.HasSuffix(imagefile, ".qcow") { - path, err := filepath.Abs(imagepath) if err != nil { @@ -167,7 +165,6 @@ func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string // Check error response if errors.Is(err, ErrQCOWDetected) { - extractpath := fmt.Sprintf("%s/%s", tmpdir, imagefile) extractpath = strings.TrimSuffix(extractpath, ".qcow2") + ".raw" @@ -192,13 +189,10 @@ func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string } return extractpath, nil - } return path, err - } else if strings.HasSuffix(imagefile, ".tar.xz") { - args = []string{ "xfvJ", imagepath, @@ -207,9 +201,7 @@ func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string } execCmd = "tar" - } else if strings.HasSuffix(imagefile, ".tar.gz") || strings.HasSuffix(imagefile, ".tgz") { - args = []string{ "xfvz", imagepath, @@ -218,9 +210,7 @@ func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string } execCmd = "tar" - } else if strings.HasSuffix(imagefile, ".tar") { - args = []string{ "xfv", imagepath, @@ -229,36 +219,31 @@ func ExtractDiskImageFromFile(imagepath string, tmpdir string) (diskimage string } execCmd = "tar" - } else if strings.HasSuffix(imagefile, ".xz") { - args = []string{ "-dk", imagepath, } execCmd = "xz" - } else { err = errors.New("unsupported filetype") - return + return diskimage, err } cmd := exec.Command(execCmd, args...) output, err := cmd.CombinedOutput() if err != nil { - return + return diskimage, err } diskimage, err = extractDiskImagePath(tmpdir, output) - return - + return diskimage, err } func extractDiskImagePath(imagedir string, output []byte) (diskimage string, err error) { - reader := bytes.NewReader(output) r := bufio.NewReader(reader) @@ -286,12 +271,10 @@ func extractDiskImagePath(imagedir string, output []byte) (diskimage string, err } } - return - + return diskimage, err } func validateDiskImagePath(diskimage string) (err error) { - args := []string{ diskimage, } @@ -305,15 +288,12 @@ func validateDiskImagePath(diskimage string) (err error) { filetype := strings.Split(string(output), ":") if len(filetype) > 1 { - if strings.Contains(filetype[1], "DOS/MBR boot sector") || strings.Contains(filetype[1], "Linux ") { return nil } else if strings.Contains(filetype[1], "QEMU QCOW") { return ErrQCOWDetected } - } return errors.New("no valid disk image found") - } diff --git a/spinifex/utils/nbd.go b/spinifex/utils/nbd.go index 50ce658f2..3efec908c 100644 --- a/spinifex/utils/nbd.go +++ b/spinifex/utils/nbd.go @@ -12,7 +12,6 @@ import ( // GenerateSocketFile generates a socket file path for the given name. // Deprecated: Use GenerateUniqueSocketFile for new code to ensure uniqueness. func GenerateSocketFile(name string) (string, error) { - if name == "" { return "", errors.New("name is required") } diff --git a/spinifex/utils/pidfile.go b/spinifex/utils/pidfile.go index cfdb0a2c1..e2471601a 100644 --- a/spinifex/utils/pidfile.go +++ b/spinifex/utils/pidfile.go @@ -12,7 +12,6 @@ import ( ) func ReadPidFile(name string) (int, error) { - pidPath := pidPath() pidFile, err := os.ReadFile(filepath.Join(pidPath, fmt.Sprintf("%s.pid", name))) @@ -28,7 +27,6 @@ func ReadPidFile(name string) (int, error) { } func GeneratePidFile(name string) (string, error) { - if name == "" { return "", errors.New("name is required") } @@ -43,7 +41,6 @@ func GeneratePidFile(name string) (string, error) { } func WritePidFile(name string, pid int) error { - // Write PID to file, check XDG, otherwise user home directory ~/spinifex/ pidFilename, err := GeneratePidFile(name) @@ -58,7 +55,7 @@ func WritePidFile(name string, pid int) error { } defer pidFile.Close() - _, err = pidFile.WriteString(fmt.Sprintf("%d", pid)) + _, err = fmt.Fprintf(pidFile, "%d", pid) if err != nil { return err } @@ -83,7 +80,7 @@ func WritePidFileTo(dir string, name string, pid int) error { } defer pidFile.Close() - _, err = pidFile.WriteString(fmt.Sprintf("%d", pid)) + _, err = fmt.Fprintf(pidFile, "%d", pid) return err } @@ -134,7 +131,6 @@ func StopProcessAt(dir string, name string) error { } func RemovePidFile(serviceName string) error { - pidPath := pidPath() err := os.Remove(filepath.Join(pidPath, fmt.Sprintf("%s.pid", serviceName))) diff --git a/spinifex/utils/process.go b/spinifex/utils/process.go index 8c9f772db..1b2c901b3 100644 --- a/spinifex/utils/process.go +++ b/spinifex/utils/process.go @@ -41,7 +41,6 @@ func StopProcess(serviceName string) error { } func KillProcess(pid int) error { - process, err := os.FindProcess(pid) if err != nil { @@ -68,7 +67,7 @@ func KillProcess(pid int) error { err = process.Signal(syscall.Signal(0)) if err != nil { - // Process terminated, break + // Signal(0) error means the process has terminated — this is the expected outcome. break } @@ -86,6 +85,5 @@ func KillProcess(pid int) error { } } - return nil - + return nil //nolint:nilerr // Signal(0) error means process exited — that's success } diff --git a/spinifex/utils/utils.go b/spinifex/utils/utils.go index 2f0a8bfc2..6a65af27e 100644 --- a/spinifex/utils/utils.go +++ b/spinifex/utils/utils.go @@ -41,7 +41,6 @@ func GenerateResourceID(prefix string) string { // Convert interface to XML func MarshalToXML(payload any) ([]byte, error) { - var buf bytes.Buffer enc := xml.NewEncoder(&buf) @@ -56,7 +55,6 @@ func MarshalToXML(payload any) ([]byte, error) { } return buf.Bytes(), nil - } // wrapWithLocation decorates payload with the requested locationName tag. @@ -106,7 +104,6 @@ func GenerateIAMXMLPayload(action string, payload any) any { // Generate JSON Error Payload func GenerateErrorPayload(code string) (jsonResponse []byte) { - var responseError ec2.ResponseError responseError.Code = aws.String(code) @@ -117,13 +114,11 @@ func GenerateErrorPayload(code string) (jsonResponse []byte) { return nil } - return - + return jsonResponse } // Validate the payload is an ec2.ResponseError func ValidateErrorPayload(payload []byte) (responseError ec2.ResponseError, err error) { - decoder := json.NewDecoder(bytes.NewReader(payload)) decoder.DisallowUnknownFields() @@ -137,13 +132,11 @@ func ValidateErrorPayload(payload []byte) (responseError ec2.ResponseError, err // Either failed to decode (not an error structure) or Code is nil (empty valid response) return responseError, nil - } // Unmarshal payload func UnmarshalJsonPayload(input any, jsonData []byte) []byte { - decoder := json.NewDecoder(bytes.NewReader(jsonData)) decoder.DisallowUnknownFields() @@ -154,11 +147,9 @@ func UnmarshalJsonPayload(input any, jsonData []byte) []byte { } return nil - } func MarshalJsonPayload(input any, jsonData []byte) []byte { - decoder := json.NewDecoder(bytes.NewReader(jsonData)) decoder.DisallowUnknownFields() @@ -169,7 +160,6 @@ func MarshalJsonPayload(input any, jsonData []byte) []byte { } return nil - } // ValidateKeyPairName validates that a key pair name only contains allowed characters: @@ -204,7 +194,6 @@ func ValidateKeyPairName(name string) error { } func CreateS3Client(cfg *config.Config) *s3.S3 { - tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, @@ -225,13 +214,11 @@ func CreateS3Client(cfg *config.Config) *s3.S3 { })) return s3.New(sess) - } // Download helper func DownloadFileWithProgress(url string, name string, filename string, timeout time.Duration) (err error) { - // Context with optional timeout and Ctrl+C cancel ctx, cancel := context.WithCancel(context.Background()) if timeout > 0 { @@ -288,10 +275,8 @@ func DownloadFileWithProgress(url string, name string, filename string, timeout } pterm.Printf("Saved %s (%s)\n", filename, humanBytes(SafeInt64ToUint64(written))) - return - + return err } else { - // Unknown size: spinner that shows bytes downloaded spin, _ := pterm.DefaultSpinner. WithText("Downloading (size unknown)..."). @@ -309,7 +294,6 @@ func DownloadFileWithProgress(url string, name string, filename string, timeout } pterm.Printf("Saved %s (%s)\n", filename, humanBytes(SafeInt64ToUint64(written))) - } return nil diff --git a/spinifex/utils/utils_test.go b/spinifex/utils/utils_test.go index 67ed3c882..6baefda3b 100644 --- a/spinifex/utils/utils_test.go +++ b/spinifex/utils/utils_test.go @@ -43,7 +43,6 @@ func TestGenerateResourceID(t *testing.T) { } func TestGeneratePidFile(t *testing.T) { - // Simulate a sample process running (e.g cat) cmd := exec.Command("cat") cmd.Start() @@ -70,12 +69,10 @@ func TestGeneratePidFile(t *testing.T) { //time.Sleep(2 * time.Second) // Simulate process ending - } func TestGenerateSocketFile(t *testing.T) { - - socketPath := fmt.Sprintf("%s/%s", os.TempDir(), "utilsunittest") + socketPath := fmt.Sprintf("%s/%s", t.TempDir(), "utilsunittest") name, err := GenerateSocketFile(socketPath) @@ -87,11 +84,9 @@ func TestGenerateSocketFile(t *testing.T) { _, err = GenerateSocketFile("") assert.Error(t, err) - } func TestExecProcessAndKill(t *testing.T) { - // Simulate a sample process running (e.g sleep, 30 secs) cmd := exec.Command("sleep", "30") @@ -142,7 +137,6 @@ func TestExecProcessAndKill(t *testing.T) { // Verify process is killed err = cmd.Process.Signal(syscall.Signal(0)) assert.Error(t, err) // Should return an error since process is killed - } func TestUnmarshalJsonPayload(t *testing.T) { @@ -593,13 +587,10 @@ func TestStopProcess(t *testing.T) { // Test file extraction process func TestExtractDiskImageFromFile(t *testing.T) { - - tmpDir, err := os.MkdirTemp("", "spx-utils-test-*") + tmpDir := t.TempDir() t.Log("Temp dir:", tmpDir) - assert.NoError(t, err, "Temp dir should be created") - // Sample .xz (fail) imagePath, err := ExtractDiskImageFromFile("/tmp/file.xz", tmpDir) @@ -631,7 +622,6 @@ func TestExtractDiskImageFromFile(t *testing.T) { _, err = exec.LookPath("tar") if err == nil { - // Sample .tgz imagePath, err = ExtractDiskImageFromFile("../../tests/unit-test-disk-image2.tgz", tmpDir) @@ -671,14 +661,12 @@ func TestExtractDiskImageFromFile(t *testing.T) { assert.Contains(t, imagePath, ".raw") assert.NoError(t, err, "Should not error") - } else { t.Skip("tar command not found, skipping archive extraction tests") } //err = os.RemoveAll(tmpDir) //assert.NoError(t, err, "Could not remove temp dir") - } func TestIsSocketURI(t *testing.T) { @@ -774,9 +762,8 @@ func TestDirExists(t *testing.T) { assert.False(t, dirExists("/nonexistent/path/should/not/exist")) // File (not a directory) - tmpFile, err := os.CreateTemp("", "direxists-test-*") + tmpFile, err := os.CreateTemp(t.TempDir(), "direxists-test-*") require.NoError(t, err) - defer os.Remove(tmpFile.Name()) tmpFile.Close() assert.False(t, dirExists(tmpFile.Name())) } diff --git a/spinifex/vm/vm.go b/spinifex/vm/vm.go index f3b947b4e..a967161d0 100644 --- a/spinifex/vm/vm.go +++ b/spinifex/vm/vm.go @@ -135,7 +135,6 @@ type Config struct { } func (cfg *Config) Execute() (*exec.Cmd, error) { - args := []string{} if cfg.PIDFile != "" { @@ -154,9 +153,7 @@ func (cfg *Config) Execute() (*exec.Cmd, error) { //slog.Warn("Setting -cpu max, `host` CPU type unavailable") // Use qemu defaults //args = append(args, "-cpu", "max") - } else { - if cfg.EnableKVM { args = append(args, "-enable-kvm") } @@ -164,7 +161,6 @@ func (cfg *Config) Execute() (*exec.Cmd, error) { if cfg.CPUType != "" { args = append(args, "-cpu", cfg.CPUType) } - } if cfg.NoGraphic { @@ -198,7 +194,6 @@ func (cfg *Config) Execute() (*exec.Cmd, error) { } for _, drive := range cfg.Drives { - var opts []string //args = append(args, "-drive", fmt.Sprintf("file=%s", drive.File) @@ -228,7 +223,6 @@ func (cfg *Config) Execute() (*exec.Cmd, error) { } args = append(args, "-drive", strings.Join(opts, ",")) - } for _, device := range cfg.Devices { @@ -267,7 +261,6 @@ func (cfg *Config) Execute() (*exec.Cmd, error) { slog.Warn("UEFI firmware file not found for ARM virt machine. Ensure qemu-efi-aarch64 package is installed.", "path", uefiPath) return nil, fmt.Errorf("UEFI firmware file not found for ARM virt machine") } - } else if cfg.MachineType != "" { args = append(args, "-M", cfg.MachineType) } diff --git a/spinifex/vm/vm_test.go b/spinifex/vm/vm_test.go index 1fa9cfb08..d41f74430 100644 --- a/spinifex/vm/vm_test.go +++ b/spinifex/vm/vm_test.go @@ -9,7 +9,6 @@ import ( ) func TestExecute(t *testing.T) { - cfg := Config{ Name: "test-vm", } @@ -74,7 +73,6 @@ func TestExecute(t *testing.T) { assert.Contains(t, cmd.Path, "qemu-system-aarch64") assert.Equal(t, expectedArgs, cmd.Args[1:]) - } func TestExecute_IOThreadAndCache(t *testing.T) {