From 4b88ffbd1523f56fbc7e22c41d8b1710237f697a Mon Sep 17 00:00:00 2001 From: Kevin Franklin Kim Date: Fri, 10 Oct 2025 11:54:31 +0200 Subject: [PATCH] feat: bump linter --- .golangci.yml | 241 ++++++------------------- .mise.toml | 2 +- Makefile | 64 ++++--- cmd/actions/bake.go | 1 + cmd/actions/build.go | 1 + cmd/actions/completion.go | 1 + cmd/actions/config.go | 2 + cmd/actions/diff.go | 1 + cmd/actions/list.go | 8 + cmd/actions/postrenderer.go | 3 +- cmd/actions/root.go | 13 ++ cmd/actions/schema.go | 3 + cmd/actions/template.go | 1 + cmd/actions/up.go | 5 + cmd/actions/version.go | 1 + internal/cmd/ptermsloghandler.go | 11 +- internal/config/build.go | 6 + internal/config/chart.go | 4 + internal/config/config.go | 5 + internal/config/map.go | 15 ++ internal/config/tags.go | 2 + internal/config/unit.go | 15 ++ internal/helm/depency.go | 4 + internal/helm/doc.go | 3 + internal/jsonschema/jsonschema.go | 11 +- internal/jsonschema/loadmap.go | 7 +- internal/pterm/multiprinter.go | 9 +- internal/pterm/noopspinner.go | 4 + internal/pterm/standardmultiprinter.go | 1 + internal/pterm/standardspinner.go | 6 + internal/template/default.go | 1 + internal/template/file.go | 13 +- internal/template/format.go | 2 + internal/template/git.go | 1 + internal/template/kubeseal.go | 7 + internal/template/onepassword.go | 15 ++ internal/template/template.go | 4 + internal/testutils/snapshot.go | 4 + internal/util/cmd.go | 29 ++- internal/util/errors.go | 4 + internal/util/highlight.go | 8 + internal/util/kube.go | 21 +++ internal/util/path.go | 4 + internal/util/template.go | 2 + internal/util/yaml.go | 4 + schema_test.go | 1 + squadron.go | 112 ++++++++++-- squadron_test.go | 3 + 48 files changed, 450 insertions(+), 235 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 59c503b..bde37b4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,122 +1,59 @@ +# https://golangci-lint.run/usage/configuration/ +# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json version: "2" run: - build-tags: [safe] + build-tags: [ safe ] modules-download-mode: readonly linters: - default: none - enable: - ## Default linters - - errcheck # errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] - - govet # (vet, vetshadow) Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes. [fast: false, auto-fix: false] - - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] - - staticcheck # (megacheck) It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. [fast: false, auto-fix: false] - - unused # (megacheck) Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] - - ## Recommended linters - - asasalint # check for pass []any as any in variadic func(...any) [fast: false, auto-fix: false] - - asciicheck # checks that all code identifiers does not have non-ASCII symbols in the name [fast: true, auto-fix: false] - - bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false] - - bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] - - canonicalheader # checks whether net/http.Header uses canonical header [fast: false, auto-fix: false] - - containedctx # containedctx is a linter that detects struct contained context.Context field [fast: false, auto-fix: false] - - contextcheck # check whether the function uses a non-inherited context [fast: false, auto-fix: false] - - copyloopvar # (go >= 1.22) copyloopvar is a linter detects places where loop variables are copied [fast: true, auto-fix: false] - - decorder # check declaration order and count of types, constants, variables and functions [fast: true, auto-fix: false] - - durationcheck # check for two durations multiplied together [fast: false, auto-fix: false] - - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false] - - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] - - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] - - exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false] - - exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions. [auto-fix] - - fatcontext # detects nested contexts in loops and function literals [fast: false, auto-fix: false] - #- forbidigo # Forbids identifiers [fast: false, auto-fix: false] - - forcetypeassert # finds forced type assertions [fast: true, auto-fix: false] - - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. [fast: true, auto-fix: false] - - gochecksumtype # Run exhaustiveness checks on Go "sum types" [fast: false, auto-fix: false] - - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - - gocritic # Provides diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: true] - - goheader # Checks is file header matches to pattern [fast: true, auto-fix: true] - - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false] - - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false] - - goprintffuncname # Checks that printf-like functions are named with `f` at the end. [fast: true, auto-fix: false] - - gosec # (gas) Inspects source code for security problems [fast: false, auto-fix: false] - - gosmopolitan # Report certain i18n/l10n anti-patterns in your Go codebase [fast: false, auto-fix: false] - - grouper # Analyze expression groups. [fast: true, auto-fix: false] - - iface # Detect the incorrect use of interfaces, helping developers avoid interface pollution. [fast: false, auto-fix: false] - - importas # Enforces consistent import aliases [fast: false, auto-fix: false] - - inamedparam # reports interfaces with unnamed method parameters [fast: true, auto-fix: false] + default: all + disable: + # Project specific linters + - forbidigo + - recvcheck + - paralleltest + # Discouraged linters + - noinlineerr # Disallows inline error handling (`if err := ...; err != nil {`). + - embeddedstructfieldcheck # Embedded types should be at the top of the field list of a struct, and there must be an empty line separating embedded fields from regular fields. [fast] + - cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false] + - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] + - dupl # Tool for code clone detection [fast: true, auto-fix: false] + - dupword # checks for duplicate words in the source code [fast: true, auto-fix: false] + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] + - err113 # Go linter to check the errors handling expressions [fast: false, auto-fix: false] + - exhaustruct # Checks if all structure fields are initialized [fast: false, auto-fix: false] + - funlen # Tool for detection of long functions [fast: true, auto-fix: false] + - ginkgolinter # enforces standards of using ginkgo and gomega [fast: false, auto-fix: false] + - gochecknoglobals # Check that no global variables exist. [fast: false, auto-fix: false] + - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] + - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] + - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] + - godot # Check if comments end in a period [fast: true, auto-fix: true] + - godox # Tool for detection of comment keywords [fast: true, auto-fix: false] + - interfacebloat # A linter that checks the number of methods inside an interface. [fast: true, auto-fix: false] - intrange # (go >= 1.22) intrange is a linter to find places where for loops could make use of an integer range. [fast: true, auto-fix: false] - - loggercheck # (logrlint) Checks key value pairs for common logger libraries (kitlog,klog,logr,zap). [fast: false, auto-fix: false] - - makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false] - - mirror # reports wrong mirror patterns of bytes/strings usage [fast: false, auto-fix: true] - - misspell # Finds commonly misspelled English words [fast: true, auto-fix: true] - - musttag # enforce field tags in (un)marshaled structs [fast: false, auto-fix: false] - - nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero). [fast: true, auto-fix: false] - - nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false] - - nilnesserr # Reports constructs that checks for err != nil, but returns a different nil value error. - - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false] - - noctx # Finds sending http request without context.Context [fast: false, auto-fix: false] - - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: true] - - nonamedreturns # Reports all named returns [fast: false, auto-fix: false] - - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. [fast: true, auto-fix: false] - #- paralleltest # Detects missing usage of t.Parallel() method in your Go test [fast: false, auto-fix: false] - - predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false] - - promlinter # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false] - - reassign # Checks that package variables are not reassigned [fast: false, auto-fix: false] - #- recvcheck # checks for receiver type consistency [fast: false, auto-fix: false] - - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false] - - rowserrcheck # checks whether Rows.Err of rows is checked successfully [fast: false, auto-fix: false] - - spancheck # Checks for mistakes with OpenTelemetry/Census spans. [fast: false, auto-fix: false] - - sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed. [fast: false, auto-fix: false] - - testableexamples # linter checks if examples are testable (have an expected output) [fast: true, auto-fix: false] - - testifylint # Checks usage of github.com/stretchr/testify. [fast: false, auto-fix: false] - - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false] - - thelper # thelper detects tests helpers which is not start with t.Helper() method. [fast: false, auto-fix: false] - - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes. [fast: false, auto-fix: false] - - unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false] - - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. [fast: true, auto-fix: false] - - usetesting # Reports uses of functions with replacement inside the testing package. [auto-fix] - - wastedassign # Finds wasted assignment statements [fast: false, auto-fix: false] - - whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc. [fast: true, auto-fix: true] + - ireturn # Accept Interfaces, Return Concrete Types [fast: false, auto-fix: false] + - lll # Reports long lines [fast: true, auto-fix: false] + - maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false] + - nestif # Reports deeply nested if statements [fast: true, auto-fix: false] + - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] + - mnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] + - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. [fast: false, auto-fix: false] + - prealloc # Finds slice declarations that could potentially be pre-allocated [fast: true, auto-fix: false] + - protogetter # Reports direct reads from proto message fields when getters should be used [fast: false, auto-fix: true] + - sloglint # ensure consistent code style when using log/slog [fast: false, auto-fix: false] + - tagalign # check that struct tags are well aligned [fast: true, auto-fix: true] + - tagliatelle # Checks the struct tags. [fast: true, auto-fix: false] + - unparam # Reports unused function parameters [fast: false, auto-fix: false] + - varnamelen # checks that the length of a variable's name matches its scope [fast: false, auto-fix: false] + - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] + - zerologlint # Detects the wrong usage of `zerolog` that a user forgets to dispatch with `Send` or `Msg` [fast: false, auto-fix: false] + # Deprected linters + - wsl - ## Discouraged linters - #- cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false] - #- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] - #- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] - #- dupl # Tool for code clone detection [fast: true, auto-fix: false] - #- dupword # checks for duplicate words in the source code [fast: true, auto-fix: false] - #- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] - #- err113 # Go linter to check the errors handling expressions [fast: false, auto-fix: false] - #- exhaustruct # Checks if all structure fields are initialized [fast: false, auto-fix: false] - #- funlen # Tool for detection of long functions [fast: true, auto-fix: false] - #- ginkgolinter # enforces standards of using ginkgo and gomega [fast: false, auto-fix: false] - #- gochecknoglobals # Check that no global variables exist. [fast: false, auto-fix: false] - #- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - #- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] - #- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - #- godot # Check if comments end in a period [fast: true, auto-fix: true] - #- godox # Tool for detection of comment keywords [fast: true, auto-fix: false] - #- interfacebloat # A linter that checks the number of methods inside an interface. [fast: true, auto-fix: false] - #- intrange # (go >= 1.22) intrange is a linter to find places where for loops could make use of an integer range. [fast: true, auto-fix: false] - #- ireturn # Accept Interfaces, Return Concrete Types [fast: false, auto-fix: false] - #- lll # Reports long lines [fast: true, auto-fix: false] - #- maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false] - #- nestif # Reports deeply nested if statements [fast: true, auto-fix: false] - #- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] - #- mnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] - #- perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. [fast: false, auto-fix: false] - #- prealloc # Finds slice declarations that could potentially be pre-allocated [fast: true, auto-fix: false] - #- protogetter # Reports direct reads from proto message fields when getters should be used [fast: false, auto-fix: true] - #- sloglint # ensure consistent code style when using log/slog [fast: false, auto-fix: false] - #- tagalign # check that struct tags are well aligned [fast: true, auto-fix: true] - #- tagliatelle # Checks the struct tags. [fast: true, auto-fix: false] - #- unparam # Reports unused function parameters [fast: false, auto-fix: false] - #- varnamelen # checks that the length of a variable's name matches its scope [fast: false, auto-fix: false] - #- wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] - #- wsl # add or remove empty lines [fast: true, auto-fix: false] - #- zerologlint # Detects the wrong usage of `zerolog` that a user forgets to dispatch with `Send` or `Msg` [fast: false, auto-fix: false] + # https://golangci-lint.run/docs/linters/ settings: exhaustive: default-signifies-exhaustive: true @@ -125,75 +62,19 @@ linters: - '!!.+' gocritic: disabled-checks: + - assignOp - ifElseChain - - commentFormatting gomoddirectives: + replace-local: true replace-allow-list: - github.com/miracl/conflate gosec: - excludes: - - G204 + severity: medium confidence: medium - importas: - no-unaliased: true - misspell: - mode: restricted - predeclared: - ignore: - - new - - error revive: - enable-all-rules: true rules: - - name: line-length-limit - disabled: true - - name: cognitive-complexity - disabled: true - name: unused-parameter disabled: true - - name: add-constant - disabled: true - - name: cyclomatic - disabled: true - - name: function-length - disabled: true - - name: function-result-limit - disabled: true - - name: flag-parameter - disabled: true - - name: unused-receiver - disabled: true - - name: argument-limit - disabled: true - - name: max-control-nesting - disabled: true - - name: comment-spacings - disabled: true - - name: struct-tag - arguments: - - json,inline - - yaml,squash - - name: unhandled-error - arguments: - - fmt.Printf - - fmt.Println - - name: deep-exit - disabled: true - - name: empty-block - disabled: true - - name: nested-structs - disabled: true - - name: unhandled-error - disabled: true - - name: indent-error-flow - disabled: true - - name: var-naming - disabled: true - - name: enforce-switch-style - disabled: true - testifylint: - disable: - - float-compare exclusions: generated: lax presets: @@ -202,12 +83,14 @@ linters: - legacy - std-error-handling rules: - - linters: - - asasalint - path: (.+)_test\.go + - path: _test\.go + linters: + - forbidigo + - unused + - gosec paths: - - bin - - tmp + - bin$ + - tmp$ - third_party$ - builtin$ - examples$ @@ -215,11 +98,3 @@ formatters: enable: - gofmt - goimports - exclusions: - generated: lax - paths: - - bin - - tmp - - third_party$ - - builtin$ - - examples$ diff --git a/.mise.toml b/.mise.toml index c06b8c5..6d9e038 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,5 +1,5 @@ [tools] # https://github.com/golangci/golangci-lint/releases -golangci-lint = "2.4.0" +golangci-lint = "2.5.0" # https://github.com/go-courier/husky/releases "github:go-courier/husky" = "1.8.1" diff --git a/Makefile b/Makefile index e0e2396..0a152fc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,15 @@ .DEFAULT_GOAL:=help -include .makerc +# --- Config ----------------------------------------------------------------- + +GOMODS=$(shell find . -type f -name go.mod) +# Newline hack for error output +define br + + +endef + # --- Targets ----------------------------------------------------------------- # This allows us to accept extra arguments @@ -9,64 +18,77 @@ .PHONY: .mise # Install dependencies +.mise: msg := $(br)$(br)Please ensure you have 'mise' installed and activated!$(br)$(br)$$ brew update$(br)$$ brew install mise$(br)$(br)See the documentation: https://mise.jdx.dev/getting-started.html$(br)$(br) .mise: - @command -v mise >/dev/null 2>&1 || { echo >&2 "Error: 'mise' is not installed or not in PATH."; exit 1; } - @mise install -q +ifeq (, $(shell command -v mise)) + $(error ${msg}) +endif + @mise install + +.PHONY: .husky +# Configure git hooks for husky +.husky: + @git config core.hooksPath .husky ### Tasks .PHONY: check -## Run tests and linters +## Run lint & tests check: tidy lint test -.PHONY: doc -## Run tests -doc: - @open "http://localhost:6060/pkg/github.com/foomo/squadron/" - @godoc -http=localhost:6060 -play - -.PHONY: test -## Run tests -test: - @# see https://github.com/pterm/pterm/issues/482 - @GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out - @#GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out -race +.PHONY: tidy +## Run go mod tidy +tidy: + @echo "〉go mod tidy" + @go mod tidy .PHONY: lint ## Run linter lint: + @echo "〉golangci-lint run" @golangci-lint run .PHONY: lint.fix ## Fix lint violations lint.fix: + @echo "〉golangci-lint run fix" @golangci-lint run --fix -.PHONY: tidy -## Run go mod tidy -tidy: - @go mod tidy +.PHONY: test +## Run tests +test: + @echo "〉go test" + @# see https://github.com/pterm/pterm/issues/482 + @GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out + @#GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out -race .PHONY: outdated ## Show outdated direct dependencies outdated: + @echo "〉go mod outdated" @go list -u -m -json all | go-mod-outdated -update -direct .PHONY: install ## Install binary install: - @echo "installing ${GOPATH}/bin/squadron" + @echo "〉installing ${GOPATH}/bin/squadron" @go build -tags=safe -o ${GOPATH}/bin/squadron cmd/main.go .PHONY: build ## Build binary build: @mkdir -p bin - @echo "building bin/squadron" + @echo "〉building bin/squadron" @go build -tags=safe -o bin/squadron cmd/main.go ### Utils +.PHONY: docs +## Open go docs +docs: + @echo "〉starting go docs" + @go doc -http + .PHONY: help ## Show help text help: diff --git a/cmd/actions/bake.go b/cmd/actions/bake.go index c87005a..8ba1058 100644 --- a/cmd/actions/bake.go +++ b/cmd/actions/bake.go @@ -50,6 +50,7 @@ func NewBake(c *viper.Viper) *cobra.Command { _ = x.BindPFlag("push", flags.Lookup("push")) cmd.Flags().Int("parallel", 1, "run command in parallel") + _ = x.BindPFlag("parallel", flags.Lookup("parallel")) flags.StringArray("bake-args", nil, "additional docker bake args") diff --git a/cmd/actions/build.go b/cmd/actions/build.go index 6d35a85..8392614 100644 --- a/cmd/actions/build.go +++ b/cmd/actions/build.go @@ -50,6 +50,7 @@ func NewBuild(c *viper.Viper) *cobra.Command { _ = x.BindPFlag("push", flags.Lookup("push")) cmd.Flags().Int("parallel", 1, "run command in parallel") + _ = x.BindPFlag("parallel", flags.Lookup("parallel")) flags.StringArray("build-args", nil, "additional docker buildx build args") diff --git a/cmd/actions/completion.go b/cmd/actions/completion.go index e33d611..307a7a1 100644 --- a/cmd/actions/completion.go +++ b/cmd/actions/completion.go @@ -64,6 +64,7 @@ PowerShell: case "powershell": return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) } + return nil }, } diff --git a/cmd/actions/config.go b/cmd/actions/config.go index 78fd709..9165cb6 100644 --- a/cmd/actions/config.go +++ b/cmd/actions/config.go @@ -26,6 +26,7 @@ func NewConfig(c *viper.Viper) *cobra.Command { if err := sq.MergeConfigFiles(cmd.Context()); err != nil { return errors.Wrap(err, "failed to merge config files") } + pterm.Debug.Println(strings.Join(append([]string{"provided files"}, files...), "\n└ ")) squadronName, unitNames := parseSquadronAndUnitNames(args) @@ -43,6 +44,7 @@ func NewConfig(c *viper.Viper) *cobra.Command { if !x.GetBool("raw") { out = util.Highlight(out) } + pterm.Println(out) return nil diff --git a/cmd/actions/diff.go b/cmd/actions/diff.go index 01abd10..7713385 100644 --- a/cmd/actions/diff.go +++ b/cmd/actions/diff.go @@ -46,6 +46,7 @@ func NewDiff(c *viper.Viper) *cobra.Command { if !x.GetBool("raw") { out = util.Highlight(out) } + pterm.Println(out) return nil diff --git a/cmd/actions/list.go b/cmd/actions/list.go index 824bec4..c793407 100644 --- a/cmd/actions/list.go +++ b/cmd/actions/list.go @@ -38,17 +38,21 @@ func NewList(c *viper.Viper) *cobra.Command { // List squadrons _ = sq.Config().Squadrons.Iterate(cmd.Context(), func(ctx context.Context, key string, value config.Map[*config.Unit]) error { list = append(list, pterm.LeveledListItem{Level: 0, Text: key}) + return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error { list = append(list, pterm.LeveledListItem{Level: 1, Text: k}) if x.GetBool("with-tags") && len(v.Tags) > 0 { list = append(list, pterm.LeveledListItem{Level: 2, Text: "🏷️: " + v.Tags.SortedString()}) } + if x.GetBool("with-charts") && len(v.Chart.String()) > 0 { list = append(list, pterm.LeveledListItem{Level: 2, Text: "📑: " + v.Chart.String()}) } + if x.GetBool("with-priority") && len(v.Chart.String()) > 0 { list = append(list, pterm.LeveledListItem{Level: 2, Text: fmt.Sprintf("☝️: %d", v.Priority)}) } + if x.GetBool("with-bakes") && len(v.Bakes) > 0 { for name, build := range v.Bakes { list = append(list, pterm.LeveledListItem{Level: 2, Text: "📦: " + name}) @@ -57,15 +61,18 @@ func NewList(c *viper.Viper) *cobra.Command { } } } + if x.GetBool("with-builds") && len(v.Builds) > 0 { for name, build := range v.Builds { list = append(list, pterm.LeveledListItem{Level: 2, Text: "📦: " + name}) + list = append(list, pterm.LeveledListItem{Level: 3, Text: build.Image + ":" + build.Tag}) for _, dependency := range build.Dependencies { list = append(list, pterm.LeveledListItem{Level: 3, Text: "🗃️: " + dependency}) } } } + return nil }) }) @@ -73,6 +80,7 @@ func NewList(c *viper.Viper) *cobra.Command { if len(list) > 0 { root := putils.TreeFromLeveledList(list) root.Text = "Squadron" + return pterm.DefaultTree.WithRoot(root).Render() } diff --git a/cmd/actions/postrenderer.go b/cmd/actions/postrenderer.go index 42c2eb1..c76e1d7 100644 --- a/cmd/actions/postrenderer.go +++ b/cmd/actions/postrenderer.go @@ -29,9 +29,10 @@ func NewPostRenderer(c *viper.Viper) *cobra.Command { return err } - c := exec.CommandContext(cmd.Context(), "kustomize", "build", args[0]) + c := exec.CommandContext(cmd.Context(), "kustomize", "build", args[0]) //nolint:gosec c.Stdout = os.Stdout c.Stderr = os.Stderr + return c.Run() }, } diff --git a/cmd/actions/root.go b/cmd/actions/root.go index 7d0788b..1e4879c 100644 --- a/cmd/actions/root.go +++ b/cmd/actions/root.go @@ -51,15 +51,18 @@ func NewRoot() *cobra.Command { if viper.GetBool("debug") { pterm.EnableDebugMessages() } + if cmd.Name() == "help" || cmd.Name() == "init" || cmd.Name() == "version" { return nil } + return util.ValidatePath(".", &cwd) }, } flags := root.PersistentFlags() flags.BoolP("debug", "d", false, "show all output") + _ = viper.BindPFlag("debug", root.PersistentFlags().Lookup("debug")) flags.StringSliceP("file", "f", []string{"squadron.yaml"}, "specify alternative squadron files") @@ -70,6 +73,7 @@ func NewRoot() *cobra.Command { func NewViper(root *cobra.Command) *viper.Viper { c := viper.New() _ = c.BindPFlag("file", root.PersistentFlags().Lookup("file")) + return c } @@ -80,22 +84,27 @@ func Execute() { if say, cerr := cowsay.Say(msg, cowsay.BallonWidth(80)); cerr == nil { msg = say } + return msg } code := 0 + defer func() { if r := recover(); r != nil { l.Error(say("It's time to panic")) l.Error(fmt.Sprintf("%v", r)) l.Error(string(debug.Stack())) + code = 1 } + os.Exit(code) }() if err := root.Execute(); err != nil { l.Error(util.SprintError(err)) + code = 1 } } @@ -109,6 +118,7 @@ func parseExtraArgs(args []string) (out []string, extraArgs []string) { //nolint return nil, args } } + return args, nil } @@ -116,11 +126,14 @@ func parseSquadronAndUnitNames(args []string) (squadron string, units []string) if len(args) == 0 { return "", nil } + if len(args) > 0 { squadron = args[0] } + if len(args) > 1 { units = args[1:] } + return squadron, units } diff --git a/cmd/actions/schema.go b/cmd/actions/schema.go index 881c4f0..a777979 100644 --- a/cmd/actions/schema.go +++ b/cmd/actions/schema.go @@ -37,15 +37,18 @@ func NewSchema(c *viper.Viper) *cobra.Command { if output := x.GetString("output"); output != "" { pterm.Info.Printfln("Writing JSON schema to %s", output) + if err := os.WriteFile(output, []byte(out), 0600); err != nil { return errors.Wrap(err, "failed to write schema") } + return nil } if !x.GetBool("raw") { out = util.Highlight(out) } + pterm.Println(out) return nil diff --git a/cmd/actions/template.go b/cmd/actions/template.go index 41bc304..34573ce 100644 --- a/cmd/actions/template.go +++ b/cmd/actions/template.go @@ -47,6 +47,7 @@ func NewTemplate(c *viper.Viper) *cobra.Command { if !x.GetBool("raw") { out = util.Highlight(out) } + pterm.Println(out) return nil diff --git a/cmd/actions/up.go b/cmd/actions/up.go index 84af251..d58d1e2 100644 --- a/cmd/actions/up.go +++ b/cmd/actions/up.go @@ -62,23 +62,28 @@ func NewUp(c *viper.Viper) *cobra.Command { Squadron: version, User: "unknown", } + if wd, err := os.Getwd(); err == nil { if value := os.Getenv("GIT_DIR"); value != "" { wd = value } + if repo, err := git.PlainOpen(wd); err == nil { if c, err := repo.Config(); err == nil { status.User = c.User.Name } + if ref, err := repo.Head(); err == nil { status.Branch = ref.Name().Short() status.Commit = ref.Hash().String() + if tags, err := repo.Tags(); err == nil { _ = tags.ForEach(func(r *plumbing.Reference) error { if r.Hash() == ref.Hash() { status.Branch = r.Name().Short() return errors.New("found tag") } + return nil }) } diff --git a/cmd/actions/version.go b/cmd/actions/version.go index e7ea3cf..d116525 100644 --- a/cmd/actions/version.go +++ b/cmd/actions/version.go @@ -16,5 +16,6 @@ func NewVersion(c *viper.Viper) *cobra.Command { pterm.Println(version) }, } + return cmd } diff --git a/internal/cmd/ptermsloghandler.go b/internal/cmd/ptermsloghandler.go index 90f69b8..f069057 100644 --- a/internal/cmd/ptermsloghandler.go +++ b/internal/cmd/ptermsloghandler.go @@ -12,6 +12,11 @@ type PTermSlogHandler struct { attrs []slog.Attr } +// NewPTermSlogHandler returns a new logging handler that can be intrgrated with log/slog. +func NewPTermSlogHandler() *PTermSlogHandler { + return &PTermSlogHandler{} +} + // Enabled returns true if the given level is enabled. func (s *PTermSlogHandler) Enabled(ctx context.Context, level slog.Level) bool { switch level { @@ -70,6 +75,7 @@ func (s *PTermSlogHandler) Handle(ctx context.Context, record slog.Record) error func (s *PTermSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { newS := *s newS.attrs = attrs + return &newS } @@ -78,8 +84,3 @@ func (s *PTermSlogHandler) WithGroup(name string) slog.Handler { // Grouping is not yet supported by pterm. return s } - -// NewPTermSlogHandler returns a new logging handler that can be intrgrated with log/slog. -func NewPTermSlogHandler() *PTermSlogHandler { - return &PTermSlogHandler{} -} diff --git a/internal/config/build.go b/internal/config/build.go index 12a1a25..0b846b4 100644 --- a/internal/config/build.go +++ b/internal/config/build.go @@ -89,12 +89,14 @@ func (b *Build) Build(ctx context.Context, squadron, unit string, args []string) cleanArgs = append(cleanArgs, strings.Split(value, " ")...) } } + argOverride := func(name string, vs string, args []string) (string, string) { if slices.ContainsFunc(args, func(s string) bool { return strings.HasPrefix(s, name) }) { return "", "" } + return name, vs } boolArgOverride := func(name string, vs bool, args []string) (string, bool) { @@ -103,10 +105,12 @@ func (b *Build) Build(ctx context.Context, squadron, unit string, args []string) }) { return "", false } + return name, vs } pterm.Debug.Printfln("running docker build for %q", b.Context) + return util.NewDockerCommand().Build(b.Context). TemplateData(map[string]string{"image": b.Image, "tag": b.Tag}). ListArg("--add-host", b.AddHost). @@ -152,7 +156,9 @@ func (b *Build) UnmarshalYAML(value *yaml.Node) error { if err := value.Decode(&vString); err != nil { return err } + b.Context = vString + return nil default: return fmt.Errorf("unsupported node tag type for %T: %q", b, value.Tag) diff --git a/internal/config/chart.go b/internal/config/chart.go index c83186b..c69f3cb 100644 --- a/internal/config/chart.go +++ b/internal/config/chart.go @@ -63,6 +63,7 @@ func (d *Chart) UnmarshalYAML(value *yaml.Node) error { if _, err := os.Stat(path.Join(schemaPath, "values.schema.json")); err == nil { d.Schema = path.Join(schemaPath, "values.schema.json") } + return nil default: return fmt.Errorf("unsupported node tag type for %T: %q", d, value.Tag) @@ -75,12 +76,15 @@ func (d *Chart) String() string { func loadChart(name string) (*Chart, error) { c := Chart{} + file, err := os.ReadFile(name) if err != nil { return nil, errors.Wrap(err, "error while opening file") } + if err := yaml.Unmarshal(file, &c); err != nil { return nil, errors.Wrap(err, "error while unmarshalling template file") } + return &c, nil } diff --git a/internal/config/config.go b/internal/config/config.go index af3a4af..fc462db 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,6 +26,7 @@ func (Config) JSONSchemaProperty(prop string) any { if prop == "squadron" { return map[string]map[string]*Unit{} } + return nil } @@ -40,15 +41,19 @@ func (c *Config) BuildDependencies(ctx context.Context) map[string]Build { if !ok { return errors.Errorf("missing build dependency `%s`", dependency) } + ret[dependency] = b } } + return nil }) }) + if len(ret) > 0 { return ret } + return nil } diff --git a/internal/config/map.go b/internal/config/map.go index d60bff5..53b1a3c 100644 --- a/internal/config/map.go +++ b/internal/config/map.go @@ -18,10 +18,12 @@ func (m Map[T]) Trim() { if val.Kind() == reflect.Ptr { val = val.Elem() } + if !val.IsValid() { delete(m, key) continue } + if val.IsZero() { delete(m, key) continue @@ -42,11 +44,14 @@ func (m Map[T]) Keys() []string { if reflect.ValueOf(m).IsZero() { return nil } + ret := make([]string, 0, len(m)) for key := range m { ret = append(ret, key) } + sort.Strings(ret) + return ret } @@ -55,11 +60,14 @@ func (m Map[T]) Values() []T { if len(m) == 0 { return nil } + keys := m.Keys() + ret := make([]T, 0, len(keys)) for i, key := range keys { ret[i] = m[key] } + return ret } @@ -67,17 +75,20 @@ func (m Map[T]) Filter(keys ...string) error { if len(keys) == 0 { return nil } + validKeys := m.Keys() for _, key := range keys { if !slices.Contains(validKeys, key) { return errors.Errorf("key not found: `%s`", key) } } + for key := range m { if !slices.Contains(keys, key) { delete(m, key) } } + return nil } @@ -87,6 +98,7 @@ func (m Map[T]) FilterFn(handler func(key string, value T) bool) error { delete(m, key) } } + return nil } @@ -94,13 +106,16 @@ func (m Map[T]) Iterate(ctx context.Context, handler func(ctx context.Context, k if len(m) == 0 { return nil } + for _, key := range m.Keys() { if err := ctx.Err(); err != nil { return err } + if err := handler(ctx, key, m[key]); err != nil { return err } } + return nil } diff --git a/internal/config/tags.go b/internal/config/tags.go index b5c8076..fbd1fbe 100644 --- a/internal/config/tags.go +++ b/internal/config/tags.go @@ -20,11 +20,13 @@ func (t Tags) Strings() []string { for i, tag := range t { ret[i] = tag.String() } + return ret } func (t Tags) SortedStrings() []string { ret := t.Strings() slices.Sort(ret) + return ret } diff --git a/internal/config/unit.go b/internal/config/unit.go index ca28fb4..1171676 100644 --- a/internal/config/unit.go +++ b/internal/config/unit.go @@ -48,6 +48,7 @@ func (u *Unit) UnmarshalYAML(value *yaml.Node) error { if err := value.Decode((*wrapper)(u)); err != nil { return err } + if u.Extends != "" { // render filename filename, err := template.ExecuteFileTemplate(context.Background(), u.Extends, nil, true) @@ -65,6 +66,7 @@ func (u *Unit) UnmarshalYAML(value *yaml.Node) error { if err := yaml.Unmarshal(defaults, &m); err != nil { return errors.Wrap(err, "failed to unmarshal defaults") } + if err := mergo.Merge(&m, u.Values, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithSliceDeepCopy); err != nil { return err } @@ -72,6 +74,7 @@ func (u *Unit) UnmarshalYAML(value *yaml.Node) error { u.Extends = "" u.Values = m } + return nil } @@ -81,6 +84,7 @@ func (Unit) JSONSchemaProperty(prop string) any { if prop == "chart" { return x } + return nil } @@ -89,11 +93,13 @@ func (u *Unit) ValuesYAML(global map[string]any) ([]byte, error) { if values == nil { values = map[string]any{} } + if global != nil { if _, ok := values["global"]; !ok { values["global"] = global } } + return yamlv2.Marshal(values) } @@ -102,7 +108,9 @@ func (u *Unit) BakeNames() []string { for name := range u.Bakes { ret = append(ret, name) } + sort.Strings(ret) + return ret } @@ -111,12 +119,15 @@ func (u *Unit) BuildNames() []string { for name := range u.Builds { ret = append(ret, name) } + sort.Strings(ret) + return ret } func (u *Unit) Template(ctx context.Context, name, squadron, unit, namespace string, global map[string]any, helmArgs []string) ([]byte, error) { var ret bytes.Buffer + valueBytes, err := u.ValuesYAML(global) if err != nil { return nil, err @@ -138,13 +149,16 @@ func (u *Unit) Template(ctx context.Context, name, squadron, unit, namespace str cmd.Args(path.Clean(strings.TrimPrefix(u.Chart.Repository, "file://"))) } else { cmd.Args(u.Chart.Name) + if u.Chart.Repository != "" { cmd.Args("--repo", u.Chart.Repository) } + if u.Chart.Version != "" { cmd.Args("--version", u.Chart.Version) } } + if out, err := cmd.Run(ctx); err != nil { return nil, errors.Wrap(err, out) } @@ -161,5 +175,6 @@ func (u *Unit) PostRendererArgs() []string { "--post-renderer-args", u.Kustomize, ) } + return ret } diff --git a/internal/helm/depency.go b/internal/helm/depency.go index ff16487..819882d 100644 --- a/internal/helm/depency.go +++ b/internal/helm/depency.go @@ -27,17 +27,21 @@ func (d *Dependency) UnmarshalYAML(value *yaml.Node) error { if err := value.Decode(&vString); err != nil { return err } + vBytes, err := template.ExecuteFileTemplate(context.Background(), vString, nil, true) if err != nil { return errors.Wrap(err, "failed to render chart string") } + localChart, err := loadChart(path.Join(string(vBytes), chartFile)) if err != nil { return errors.New("failed to load local chart: " + vString) } + d.Name = localChart.Name d.Repository = fmt.Sprintf("file://%v", vString) d.Version = localChart.Version + return nil default: return fmt.Errorf("unsupported node tag type for %T: %q", d, value.Tag) diff --git a/internal/helm/doc.go b/internal/helm/doc.go index aa5b86f..ac55683 100644 --- a/internal/helm/doc.go +++ b/internal/helm/doc.go @@ -16,12 +16,15 @@ const ( func loadChart(path string) (*Chart, error) { c := Chart{} + file, err := os.ReadFile(path) if err != nil { return nil, errors.Wrap(err, "error while opening file") } + if err := yaml.Unmarshal(file, &c); err != nil { return nil, errors.Wrap(err, "error while unmarshalling template file") } + return &c, nil } diff --git a/internal/jsonschema/jsonschema.go b/internal/jsonschema/jsonschema.go index d116347..c3f5ee8 100644 --- a/internal/jsonschema/jsonschema.go +++ b/internal/jsonschema/jsonschema.go @@ -24,12 +24,14 @@ func (js *JSONSchema) LoadBaseSchema(ctx context.Context, url string) error { if err != nil { return err } + js.baseSchema = baseSchema + return nil } // SetSquadronUnitSchema overrides the base schema at the given path with another JSON schema from a URL -func (js *JSONSchema) SetSquadronUnitSchema(ctx context.Context, squardon, unit, url string) error { +func (js *JSONSchema) SetSquadronUnitSchema(ctx context.Context, squadron, unit, url string) error { var ref string if strings.HasPrefix(url, "http") { ref = strings.TrimPrefix(url, "https:") @@ -41,6 +43,7 @@ func (js *JSONSchema) SetSquadronUnitSchema(ctx context.Context, squardon, unit, ref = strings.TrimPrefix(ref, ".") ref = strings.TrimPrefix(ref, "/") } + ref = strings.TrimSuffix(ref, "/") ref = strings.ReplaceAll(ref, "/", "-") ref = strings.ToLower(ref) @@ -54,6 +57,7 @@ func (js *JSONSchema) SetSquadronUnitSchema(ctx context.Context, squardon, unit, if err != nil { return errors.Wrap(err, "failed to load map: "+url) } + delete(valuesMap, "$schema") js.ensure(defsMap, ref, valuesMap) } @@ -63,7 +67,7 @@ func (js *JSONSchema) SetSquadronUnitSchema(ctx context.Context, squardon, unit, configPropertiesMap := js.ensure(configMap, "properties", map[string]any{}) squadronsMap := js.ensure(configPropertiesMap, "squadron", map[string]any{}) squadronsPropertiesMap := js.ensure(squadronsMap, "properties", map[string]any{}) - squadronMap := js.ensure(squadronsPropertiesMap, squardon, map[string]any{ + squadronMap := js.ensure(squadronsPropertiesMap, squadron, map[string]any{ "additionalProperties": map[string]any{ "$ref": "#/$defs/Unit", }, @@ -95,6 +99,7 @@ func (js *JSONSchema) String() (string, error) { if err != nil { return "", err } + return string(output), nil } @@ -104,6 +109,7 @@ func (js *JSONSchema) PrettyString() (string, error) { if err != nil { return "", err } + return string(output), nil } @@ -113,5 +119,6 @@ func (js *JSONSchema) ensure(source map[string]any, name string, initial map[str ret = initial source[name] = ret } + return ret } diff --git a/internal/jsonschema/loadmap.go b/internal/jsonschema/loadmap.go index bb61df5..8aa9b4e 100644 --- a/internal/jsonschema/loadmap.go +++ b/internal/jsonschema/loadmap.go @@ -13,11 +13,14 @@ import ( // LoadMap fetches the JSON schema from a given URL func LoadMap(ctx context.Context, url string) (map[string]any, error) { - var err error - var body []byte + var ( + err error + body []byte + ) if strings.HasPrefix(url, "http") { pterm.Debug.Printfln("Loading map from %s", url) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err diff --git a/internal/pterm/multiprinter.go b/internal/pterm/multiprinter.go index 01ed380..542f987 100644 --- a/internal/pterm/multiprinter.go +++ b/internal/pterm/multiprinter.go @@ -12,15 +12,20 @@ type MultiPrinter interface { } func MustNewMultiPrinter() MultiPrinter { - var err error - var value MultiPrinter + var ( + err error + value MultiPrinter + ) + if _, ok := os.LookupEnv("CI"); ok { value, err = NewNoopMultiPrinter() } else { value, err = NewStandardMultiPrinter() } + if err != nil { pterm.Fatal.Print(err) } + return value } diff --git a/internal/pterm/noopspinner.go b/internal/pterm/noopspinner.go index 7afc783..7ef8e2c 100644 --- a/internal/pterm/noopspinner.go +++ b/internal/pterm/noopspinner.go @@ -55,6 +55,7 @@ func (s *NoopSpinner) Write(p []byte) (int, error) { lines = append(lines, line) } } + s.log = append(s.log, lines...) // pterm.UpdateText.Println(s.message()) return len(p), nil @@ -65,11 +66,14 @@ func (s *NoopSpinner) message(message ...string) string { if !s.start.IsZero() && s.stopped { msg[0] += " ⏱ " + time.Since(s.start).Truncate(time.Second).String() } + if value := strings.Join(message, " "); len(value) > 0 { msg = append(msg, value) } + if pterm.PrintDebugMessages { msg = append(msg, s.log...) } + return strings.Join(msg, "\n ") } diff --git a/internal/pterm/standardmultiprinter.go b/internal/pterm/standardmultiprinter.go index 9abb133..80910d1 100644 --- a/internal/pterm/standardmultiprinter.go +++ b/internal/pterm/standardmultiprinter.go @@ -15,6 +15,7 @@ func NewStandardMultiPrinter() (*StandardMultiPrinter, error) { if err != nil { return nil, err } + return &StandardMultiPrinter{printer: printer}, nil } diff --git a/internal/pterm/standardspinner.go b/internal/pterm/standardspinner.go index cdaf156..a20dcae 100644 --- a/internal/pterm/standardspinner.go +++ b/internal/pterm/standardspinner.go @@ -65,7 +65,9 @@ func (s *StandardSpinner) Write(p []byte) (int, error) { lines = append(lines, line) } } + s.log = append(s.log, lines...) + return len(p), nil } @@ -74,17 +76,21 @@ func (s *StandardSpinner) message(message ...string) string { if !s.start.IsZero() && s.stopped { msg[0] += " ⏱ " + time.Since(s.start).Truncate(time.Second).String() } + width := pterm.GetTerminalWidth() - 10 for i, line := range msg { if len(line) > width { msg[i] = line[:width] + "…" } } + if value := strings.Join(message, " "); len(value) > 0 { msg = append(msg, value) } + if pterm.PrintDebugMessages { msg = append(msg, s.log...) } + return strings.Join(msg, "\n ") } diff --git a/internal/template/default.go b/internal/template/default.go index 59eda5c..7a38556 100644 --- a/internal/template/default.go +++ b/internal/template/default.go @@ -5,5 +5,6 @@ func defaultIndexValue(v map[string]any, index string, def any) any { if _, ok = v[index]; ok { return v[index] } + return def } diff --git a/internal/template/file.go b/internal/template/file.go index a1dd02a..41e4434 100644 --- a/internal/template/file.go +++ b/internal/template/file.go @@ -41,18 +41,21 @@ func toYAML(v any) string { if err != nil { return err.Error() } + return strings.TrimSuffix(string(data), "\n") } func toYAMLPretty(v any) string { var data bytes.Buffer + encoder := yaml.NewEncoder(&data) encoder.SetIndent(2) - err := encoder.Encode(v) + err := encoder.Encode(v) if err != nil { return err.Error() } + return strings.TrimSuffix(data.String(), "\n") } @@ -68,6 +71,7 @@ func fromYAML(str string) map[string]any { if err := yaml.Unmarshal([]byte(str), &m); err != nil { m["Error"] = err.Error() } + return m } @@ -82,6 +86,7 @@ func fromYAMLArray(str string) []any { if err := yaml.Unmarshal([]byte(str), &a); err != nil { a = []any{err.Error()} } + return a } @@ -92,10 +97,12 @@ func fromYAMLArray(str string) []any { func toTOML(v any) string { b := bytes.NewBuffer(nil) e := toml.NewEncoder(b) + err := e.Encode(v) if err != nil { return err.Error() } + return b.String() } @@ -111,6 +118,7 @@ func fromTOML(str string) map[string]any { if err := toml.Unmarshal([]byte(str), &m); err != nil { m["Error"] = err.Error() } + return m } @@ -123,6 +131,7 @@ func toJSON(v any) string { if err != nil { return err.Error() } + return string(data) } @@ -138,6 +147,7 @@ func fromJSON(str string) map[string]any { if err := json.Unmarshal([]byte(str), &m); err != nil { m["Error"] = err.Error() } + return m } @@ -152,5 +162,6 @@ func fromJSONArray(str string) []any { if err := json.Unmarshal([]byte(str), &a); err != nil { a = []any{err.Error()} } + return a } diff --git a/internal/template/format.go b/internal/template/format.go index ae8f749..0cf54a6 100644 --- a/internal/template/format.go +++ b/internal/template/format.go @@ -12,6 +12,7 @@ func quote(str ...any) string { out = append(out, fmt.Sprintf("%v", s)) } } + return "'" + strings.Join(out, " ") + "'" } @@ -22,6 +23,7 @@ func quoteAll(str ...any) string { out = append(out, fmt.Sprintf("'%v'", s)) } } + return strings.Join(out, " ") } diff --git a/internal/template/git.go b/internal/template/git.go index 78aa406..bd57cec 100644 --- a/internal/template/git.go +++ b/internal/template/git.go @@ -18,6 +18,7 @@ func git(ctx context.Context) func(action string) (string, error) { default: cmd.Args = append(cmd.Args, "describe", "--tags", "--always") } + res, err := cmd.Output() if err != nil { return "", err diff --git a/internal/template/kubeseal.go b/internal/template/kubeseal.go index 7c7244f..09b68e7 100644 --- a/internal/template/kubeseal.go +++ b/internal/template/kubeseal.go @@ -14,6 +14,7 @@ import ( func kubeseal(ctx context.Context) func(values ...string) (string, error) { return func(values ...string) (string, error) { var value string + if len(values) == 0 { return "", errors.Errorf("missing value") } else if len(values) == 1 { @@ -21,6 +22,7 @@ func kubeseal(ctx context.Context) func(values ...string) (string, error) { } else { value, values = values[len(values)-1], values[:len(values)-1] } + cmd := exec.CommandContext(ctx, "kubeseal", "--raw", "--from-file=/dev/stdin") cmd.Args = append(cmd.Args, values...) cmd.Env = os.Environ() @@ -29,15 +31,19 @@ func kubeseal(ctx context.Context) func(values ...string) (string, error) { if v := os.Getenv("SQUADRON_KUBESEAL_NAME"); v != "" { cmd.Args = append(cmd.Args, "--name", v) } + if v := os.Getenv("SQUADRON_KUBESEAL_NAMESPACE"); v != "" { cmd.Args = append(cmd.Args, "--namespace", v) } + if v := os.Getenv("SQUADRON_KUBESEAL_CONTROLLER_NAME"); v != "" { cmd.Args = append(cmd.Args, "--controller-name", v) } + if v := os.Getenv("SQUADRON_KUBESEAL_CONTROLLER_NAMESPACE"); v != "" { cmd.Args = append(cmd.Args, "--controller-namespace", v) } + if v := os.Getenv("SQUADRON_KUBESEAL_EXTRA_ARGS"); v != "" { cmd.Args = append(cmd.Args, strings.Split(v, " ")...) } @@ -46,6 +52,7 @@ func kubeseal(ctx context.Context) func(values ...string) (string, error) { if err != nil { pterm.Debug.Println(cmd.String()) pterm.Error.Println(string(res)) + return "", err } diff --git a/internal/template/onepassword.go b/internal/template/onepassword.go index 2b9b09c..311f061 100644 --- a/internal/template/onepassword.go +++ b/internal/template/onepassword.go @@ -28,6 +28,7 @@ var ErrOnePasswordNotSignedIn = errors.New("not signed in") func onePasswordConnectGet(client connect.Client, vaultUUID, itemUUID string) (map[string]string, error) { var item *onepassword.Item + if onePasswordUUID.MatchString(itemUUID) { if v, err := client.GetItem(itemUUID, vaultUUID); err != nil { return nil, err @@ -52,6 +53,7 @@ func onePasswordConnectGet(client connect.Client, vaultUUID, itemUUID string) (m func onePasswordConnectGetDocument(client connect.Client, vaultUUID, itemUUID string) (string, error) { var item *onepassword.Item + if onePasswordUUID.MatchString(itemUUID) { if v, err := client.GetItem(itemUUID, vaultUUID); err != nil { return "", err @@ -85,6 +87,7 @@ var onePasswordGetLock sync.Mutex func onePasswordGet(ctx context.Context, account, vaultUUID, itemUUID string) (map[string]string, error) { onePasswordGetLock.Lock() defer onePasswordGetLock.Unlock() + var v struct { Vault struct { ID string `json:"id"` @@ -106,6 +109,7 @@ func onePasswordGet(ctx context.Context, account, vaultUUID, itemUUID string) (m return nil, errors.Errorf("wrong vault UUID %s for item %s", vaultUUID, itemUUID) } else { ret := map[string]string{} + aliases := map[string]string{ "notesPlain": "notes", } @@ -116,6 +120,7 @@ func onePasswordGet(ctx context.Context, account, vaultUUID, itemUUID string) (m ret[field.Label] = fmt.Sprintf("%v", field.Value) } } + return ret, nil } } @@ -125,12 +130,14 @@ var onePasswordGetDocumentLock sync.Mutex func onePasswordGetDocument(ctx context.Context, account, vaultUUID, itemUUID string) (string, error) { onePasswordGetDocumentLock.Lock() defer onePasswordGetDocumentLock.Unlock() + res, err := exec.CommandContext(ctx, "op", "document", "get", itemUUID, "--vault", vaultUUID, "--account", account).CombinedOutput() if err != nil && strings.Contains(string(res), "You are not currently signed in") { return "", ErrOnePasswordNotSignedIn } else if err != nil { return "", errors.Wrap(err, string(res)) } + return strings.Trim(string(res), "\n"), nil } @@ -142,6 +149,7 @@ func onePasswordSignIn(ctx context.Context, account string) error { // use multi writer to handle password prompt var stdoutBuf bytes.Buffer + cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) cmd.Stdin = os.Stdin @@ -155,6 +163,7 @@ func onePasswordSignIn(ctx context.Context, account string) error { if token := strings.TrimSuffix(stdoutBuf.String(), "\n"); token == "" { fmt.Printf("Failed to login into your '%s' account! Please refer to the manual:\n", account) fmt.Println("https://support.1password.com/command-line-getting-started/#set-up-the-command-line-tool") + return errors.New("failed to retrieve 1password session token") } else if err := os.Setenv(fmt.Sprintf("OP_SESSION_%s", account), token); err != nil { return err @@ -162,6 +171,7 @@ func onePasswordSignIn(ctx context.Context, account string) error { fmt.Println("NOTE: If you want to skip this step, run:") fmt.Printf("export OP_SESSION_%s=%s\n", account, token) } + return nil } @@ -195,6 +205,7 @@ func onePasswordInit(ctx context.Context, account string) error { if _, err := exec.LookPath("op"); err != nil { pterm.Warning.Println("Your templates includes a call to 1Password, please install it:") pterm.Warning.Println("https://support.1password.com/command-line-getting-started/#set-up-the-command-line-tool") + return errors.Wrap(err, "failed to lookup op") } @@ -225,6 +236,7 @@ func onePassword(ctx context.Context, templateVars any, errorOnMissing bool) fun } else { itemUUID = value } + if value, err := onePasswordRender("op", field, templateVars, errorOnMissing); err != nil { return "", err } else { @@ -240,6 +252,7 @@ func onePassword(ctx context.Context, templateVars any, errorOnMissing bool) fun if err != nil { return "", err } + if res, err := onePasswordConnectGet(client, vaultUUID, itemUUID); err != nil { return "", err } else { @@ -310,11 +323,13 @@ func onePasswordRender(name, text string, data any, errorOnMissing bool) (string if !errorOnMissing { opts = append(opts, "missingkey=error") } + out := bytes.NewBuffer([]byte{}) if uuidTpl, err := template.New(name).Option(opts...).Parse(text); err != nil { return "", err } else if err := uuidTpl.Execute(out, data); err != nil { return "", err } + return out.String(), nil } diff --git a/internal/template/template.go b/internal/template/template.go index eb68830..a51fbf4 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -43,12 +43,16 @@ func ExecuteFileTemplate(ctx context.Context, text string, templateVars any, err if err != nil { return nil, err } + out := bytes.NewBuffer([]byte{}) + if errorOnMissing { tpl = tpl.Option("missingkey=error") } + if err := tpl.Funcs(funcMap).Execute(out, templateVars); err != nil { return nil, err } + return out.Bytes(), nil } diff --git a/internal/testutils/snapshot.go b/internal/testutils/snapshot.go index fd466b5..8a26e71 100644 --- a/internal/testutils/snapshot.go +++ b/internal/testutils/snapshot.go @@ -12,10 +12,12 @@ import ( // Snapshot compares v with its snapshot file func Snapshot(t *testing.T, name, yaml string) { t.Helper() + snapshot := readSnapshot(t, name) if *UpdateFlag || snapshot == "" { writeSnapshot(t, name, yaml) } + assert.YAMLEq(t, snapshot, yaml) } @@ -28,9 +30,11 @@ func writeSnapshot(t *testing.T, name string, content string) { // readSnapshot reads the snapshot file for a given test t. func readSnapshot(t *testing.T, name string) string { t.Helper() + g, err := os.ReadFile(name) if !errors.Is(err, os.ErrNotExist) { require.NoError(t, err, "failed reading file", name) } + return string(g) } diff --git a/internal/util/cmd.go b/internal/util/cmd.go index 9f38c74..8e88010 100644 --- a/internal/util/cmd.go +++ b/internal/util/cmd.go @@ -42,8 +42,10 @@ func (c *Cmd) Args(args ...string) *Cmd { if arg == "" { continue } + c.append(arg) } + return c } @@ -56,7 +58,9 @@ func (c *Cmd) Arg(name, v string) *Cmd { if name == "" || v == "" { return c } + c.append(name, v) + return c } @@ -64,7 +68,9 @@ func (c *Cmd) BoolArg(name string, v bool) *Cmd { if name == "" || !v { return c } + c.append(name) + return c } @@ -72,12 +78,15 @@ func (c *Cmd) ListArg(name string, vs []string) *Cmd { if name == "" { return c } + for _, v := range vs { if v == "" { continue } + c.append(name, v) } + return c } @@ -100,7 +109,9 @@ func (c *Cmd) Stdout(w io.Writer) *Cmd { if w == nil { w, _ = os.Open(os.DevNull) } + c.stdoutWriters = append(c.stdoutWriters, w) + return c } @@ -108,28 +119,35 @@ func (c *Cmd) Stderr(w io.Writer) *Cmd { if w == nil { w, _ = os.Open(os.DevNull) } + c.stderrWriters = append(c.stderrWriters, w) + return c } func (c *Cmd) String() string { - cmd := exec.Command(c.command[0], c.command[1:]...) //nolint:noctx + cmd := exec.Command(c.command[0], c.command[1:]...) //nolint:noctx,gosec + cmd.Env = append(os.Environ(), c.env...) if c.cwd != "" { cmd.Dir = c.cwd } + return cmd.String() } func (c *Cmd) Run(ctx context.Context) (string, error) { - cmd := exec.CommandContext(ctx, c.command[0], c.command[1:]...) + cmd := exec.CommandContext(ctx, c.command[0], c.command[1:]...) //nolint:gosec + cmd.Env = append(os.Environ(), c.env...) if c.cwd != "" { cmd.Dir = c.cwd } - var stdout bytes.Buffer - var stderr bytes.Buffer + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) if c.stdin != nil { cmd.Stdin = c.stdin @@ -144,10 +162,12 @@ func (c *Cmd) Run(ctx context.Context) (string, error) { cmd.Stderr = io.MultiWriter(append(c.stderrWriters, &stderr)...) pterm.Debug.Println("❯ " + cmd.String()) + err := cmd.Run() if err != nil { err = errors.Wrap(err, "failed to execute: "+cmd.String()) } + return stdout.String() + stderr.String(), err } @@ -165,5 +185,6 @@ func (c *Cmd) append(v ...string) { } } } + c.command = append(c.command, v...) } diff --git a/internal/util/errors.go b/internal/util/errors.go index ced4896..2b3c225 100644 --- a/internal/util/errors.go +++ b/internal/util/errors.go @@ -10,7 +10,9 @@ import ( func SprintError(err error) string { var ret string + prefix := "Error: " + if pterm.PrintDebugMessages { return fmt.Sprintf("%+v", err) + "\n" } @@ -21,10 +23,12 @@ func SprintError(err error) string { ret += prefix + err.Error() + "\n" break } + if err.Error() != w.Error() { ret += prefix + strings.TrimSuffix(err.Error(), ": "+w.Error()) + "\n" prefix = "↪ " } + err = w } diff --git a/internal/util/highlight.go b/internal/util/highlight.go index 9b46880..93c557b 100644 --- a/internal/util/highlight.go +++ b/internal/util/highlight.go @@ -21,9 +21,11 @@ func Highlight(source string) string { if l == nil { l = lexers.Analyse(source) } + if l == nil { l = lexers.Fallback } + l = chroma.Coalesce(l) // Determine formatter. @@ -60,9 +62,11 @@ func HighlightHCL(source string) string { if l == nil { l = lexers.Analyse(source) } + if l == nil { l = lexers.Fallback } + l = chroma.Coalesce(l) // Determine formatter. @@ -109,6 +113,7 @@ func (w *numberWriter) Write(p []byte) (int, error) { ) for i, c := range original { tokenLen++ + if c != '\n' { continue } @@ -121,11 +126,13 @@ func (w *numberWriter) Write(p []byte) (int, error) { if w.currentLine > 9999 { format = "%d |\t%s%s" } + format = "\033[34m" + format + "\033[0m" if _, err := fmt.Fprintf(w.w, format, w.currentLine, string(w.buf), string(token)); err != nil { return i + 1, err } + w.buf = w.buf[:0] w.currentLine++ } @@ -133,5 +140,6 @@ func (w *numberWriter) Write(p []byte) (int, error) { if len(p) > 0 { w.buf = append(w.buf, p...) } + return len(original), nil } diff --git a/internal/util/kube.go b/internal/util/kube.go index 0b9457d..d6dd8ef 100644 --- a/internal/util/kube.go +++ b/internal/util/kube.go @@ -33,6 +33,7 @@ func (c KubeCmd) GetMostRecentPodBySelectors(ctx context.Context, selectors map[ for k, v := range selectors { selector = append(selector, fmt.Sprintf("%v=%v", k, v)) } + out, err := c.Args("--selector", strings.Join(selector, ","), "get", "pods", "--sort-by=.status.startTime", "-o", "name").Run(ctx) if err != nil { @@ -43,9 +44,11 @@ func (c KubeCmd) GetMostRecentPodBySelectors(ctx context.Context, selectors map[ if err != nil { return "", err } + if len(pods) > 0 { return pods[len(pods)-1], nil } + return "", errors.New("no pods found") } @@ -78,6 +81,7 @@ func (c KubeCmd) ExposePod(pod string, host string, port int) *Cmd { if host == "127.0.0.1" { host = "" } + return c.Args("expose", "pod", pod, "--type=LoadBalancer", fmt.Sprintf("--port=%v", port), fmt.Sprintf("--external-ip=%v", host)) } @@ -91,10 +95,12 @@ func (c KubeCmd) GetDeployment(ctx context.Context, deployment string) (*k8s.Dep if err != nil { return nil, err } + var d k8s.Deployment if err := json.Unmarshal([]byte(out), &d); err != nil { return nil, err } + return &d, nil } @@ -103,6 +109,7 @@ func (c KubeCmd) GetNamespaces(ctx context.Context) ([]string, error) { if err != nil { return nil, err } + return parseResources(out, "namespace/") } @@ -111,6 +118,7 @@ func (c KubeCmd) GetDeployments(ctx context.Context) ([]string, error) { if err != nil { return nil, err } + return parseResources(out, "deployment.apps/") } @@ -119,12 +127,14 @@ func (c KubeCmd) GetPods(ctx context.Context, selectors map[string]string) ([]st for k, v := range selectors { selector = append(selector, fmt.Sprintf("%v=%v", k, v)) } + out, err := c.Args("--selector", strings.Join(selector, ","), "get", "pods", "--sort-by=.status.startTime", "-o", "name").Run(ctx) if err != nil { return nil, err } + return parseResources(out, "pod/") } @@ -133,6 +143,7 @@ func (c KubeCmd) GetContainers(deployment k8s.Deployment) []string { for i, c := range deployment.Spec.Template.Spec.Containers { containers[i] = c.Name } + return containers } @@ -141,6 +152,7 @@ func (c KubeCmd) GetPodsByLabels(ctx context.Context, labels []string) ([]string if err != nil { return nil, err } + return parseResources(out, "pod/") } @@ -154,9 +166,11 @@ func (c KubeCmd) CreateConfigMapFromFile(ctx context.Context, name, path string) func (c KubeCmd) CreateConfigMap(ctx context.Context, name string, keyMap map[string]string) (string, error) { c.Args("create", "configmap", name) + for key, value := range keyMap { c.Args(fmt.Sprintf("--from-literal=%v=%v", key, value)) } + return c.Run(ctx) } @@ -172,9 +186,11 @@ func (c KubeCmd) GetConfigMapKey(ctx context.Context, name, key string) (string, if err != nil { return out, err } + if out == "" { return out, fmt.Errorf("no key %q found in ConfigMap %q", key, name) } + return out, nil } @@ -183,19 +199,24 @@ func parseResources(out, prefix string) ([]string, error) { if out == "" { return res, nil } + lines := strings.Split(out, "\n") if len(lines) == 1 && lines[0] == "" { return nil, fmt.Errorf("delimiter %q not found in %q", "\n", out) } + for _, line := range lines { if line == "" { continue } + unprefixed := strings.TrimPrefix(line, prefix) if unprefixed == line { return nil, fmt.Errorf("prefix %q not found in %q", prefix, line) } + res = append(res, strings.TrimPrefix(line, prefix)) } + return res, nil } diff --git a/internal/util/path.go b/internal/util/path.go index 26f81a3..3d7fec5 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -10,14 +10,18 @@ func ValidatePath(wd string, p *string) error { if !filepath.IsAbs(*p) { *p = path.Join(wd, *p) } + absPath, err := filepath.Abs(*p) if err != nil { return err } + _, err = os.Stat(absPath) if err != nil { return err } + *p = absPath + return nil } diff --git a/internal/util/template.go b/internal/util/template.go index 3d1dc2b..a6f93eb 100644 --- a/internal/util/template.go +++ b/internal/util/template.go @@ -12,9 +12,11 @@ func RenderTemplateString(s string, data any) (string, error) { if err != nil { return "", errors.Wrap(err, "failed to parse template") } + var out bytes.Buffer if err := t.Execute(&out, data); err != nil { return "", errors.Wrap(err, "failed to execute template") } + return out.String(), nil } diff --git a/internal/util/yaml.go b/internal/util/yaml.go index e78deba..510349f 100644 --- a/internal/util/yaml.go +++ b/internal/util/yaml.go @@ -11,15 +11,19 @@ func GenerateYaml(path string, data any) (err error) { if marshalErr != nil { return marshalErr } + file, crateErr := os.Create(path) if crateErr != nil { return crateErr } + defer func() { if closeErr := file.Close(); err == nil { err = closeErr } }() + _, err = file.Write(out) + return err } diff --git a/schema_test.go b/schema_test.go index 82b69a8..db25371 100644 --- a/schema_test.go +++ b/schema_test.go @@ -30,6 +30,7 @@ func TestSchema(t *testing.T) { require.NoError(t, err) filename := path.Join(cwd, "squadron.schema.json") + expected, err := os.ReadFile(filename) if !errors.Is(err, os.ErrNotExist) { require.NoError(t, err) diff --git a/squadron.go b/squadron.go index 48baca4..b1ebf3f 100644 --- a/squadron.go +++ b/squadron.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "maps" "os" "os/exec" "path" @@ -26,7 +27,6 @@ import ( "github.com/pkg/errors" "github.com/pterm/pterm" "github.com/sters/yaml-diff/yamldiff" - "golang.org/x/exp/maps" "golang.org/x/sync/errgroup" yamlv2 "gopkg.in/yaml.v2" "gopkg.in/yaml.v3" @@ -59,6 +59,7 @@ func New(basePath, namespace string, files []string) *Squadron { func (sq *Squadron) Namespace(ctx context.Context, squadron, unit string, u *config.Unit) (string, error) { var tpl string + switch { case u.Namespace != "": tpl = u.Namespace @@ -67,6 +68,7 @@ func (sq *Squadron) Namespace(ctx context.Context, squadron, unit string, u *con default: return "default", nil } + return util.RenderTemplateString(tpl, map[string]string{"Squadron": squadron, "Unit": unit}) } @@ -84,12 +86,14 @@ func (sq *Squadron) ConfigYAML() string { func (sq *Squadron) MergeConfigFiles(ctx context.Context) error { start := time.Now() + pterm.Info.Println("📚 | merging configs") mergedFiles, err := conflate.FromFiles(sq.files...) if err != nil { return errors.Wrap(err, "failed to conflate files") } + fileBytes, err := mergedFiles.MarshalYAML() if err != nil { return errors.Wrap(err, "failed to marshal yaml") @@ -99,6 +103,7 @@ func (sq *Squadron) MergeConfigFiles(ctx context.Context) error { pterm.Error.Println(string(fileBytes)) return errors.Wrap(err, "failed to unmarshal yaml") } + if sq.c.Version != config.Version { pterm.Debug.Println(string(fileBytes)) return errors.New("Please upgrade your YAML definition to from '" + sq.c.Version + "' to '" + config.Version + "'") @@ -115,6 +120,7 @@ func (sq *Squadron) MergeConfigFiles(ctx context.Context) error { sq.config = string(value) pterm.Success.Println("📚 | merging configs ⏱ " + time.Since(start).Truncate(time.Second).String()) + return nil } @@ -164,10 +170,14 @@ func (sq *Squadron) FilterConfig(ctx context.Context, squadron string, units, ta func (sq *Squadron) RenderConfig(ctx context.Context) error { start := time.Now() + pterm.Info.Println("📗 | rendering config") - var tv templatex.Vars - var vars map[string]any + var ( + tv templatex.Vars + vars map[string]any + ) + if err := yaml.Unmarshal([]byte(sq.config), &vars); err != nil { return errors.Wrap(err, "failed to render config") } @@ -177,9 +187,11 @@ func (sq *Squadron) RenderConfig(ctx context.Context) error { if value, ok := vars["global"]; ok { tv.Add("Global", value) } + if value, ok := vars["vars"]; ok { tv.Add("Vars", value) } + if value, ok := vars["squadron"]; ok { tv.Add("Squadron", value) } @@ -206,9 +218,11 @@ func (sq *Squadron) RenderConfig(ctx context.Context) error { if value, ok := vars["global"]; ok { tv.Add("Global", value) } + if value, ok := vars["vars"]; ok { tv.Add("Vars", value) } + if value, ok := vars["squadron"]; ok { tv.Add("Squadron", value) } @@ -245,6 +259,7 @@ func (sq *Squadron) Push(ctx context.Context, pushArgs []string, parallel int) e item any image string } + var all []one _ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error { @@ -261,6 +276,7 @@ func (sq *Squadron) Push(ctx context.Context, pushArgs []string, parallel int) e }) spinner.Start() } + for _, name := range v.BakeNames() { bake := v.Bakes[name] for _, tag := range bake.Tags { @@ -275,6 +291,7 @@ func (sq *Squadron) Push(ctx context.Context, pushArgs []string, parallel int) e spinner.Start() } } + return nil }) }) @@ -297,13 +314,16 @@ func (sq *Squadron) Push(ctx context.Context, pushArgs []string, parallel int) e cleanArgs = append(cleanArgs, strings.Split(value, " ")...) } } + pterm.Debug.Printfln("running docker push for %s", a.image) + if out, err := util.NewDockerCommand().Push(a.image).Args(cleanArgs...).Run(ctx); err != nil { a.spinner.Fail(out) return err } a.spinner.Success() + return nil }) } @@ -334,6 +354,7 @@ func (sq *Squadron) BuildDependencies(ctx context.Context, buildArgs []string, p } spinner.Success() + return nil } @@ -403,10 +424,13 @@ func (sq *Squadron) Bake(ctx context.Context, buildArgs []string) error { g.Targets = append(g.Targets, item.Name) c.Targets = append(c.Targets, &item) } + return nil }) + return nil }) + c.Groups = append(c.Groups, g) b, err := dethcl.Marshal(c) @@ -414,10 +438,13 @@ func (sq *Squadron) Bake(ctx context.Context, buildArgs []string) error { if err != nil { return errors.Wrap(err, "failed to marshal bake config") } + pterm.Debug.Println("🔥 | bakefile:\n" + util.HighlightHCL(string(b))) start := time.Now() + pterm.Success.Println("🔥 | baking containers") + out, err := util.NewDockerCommand(). Bake(bytes.NewReader(b)). Stderr(ptermx.NewWriter(pterm.Debug)). @@ -426,6 +453,7 @@ func (sq *Squadron) Bake(ctx context.Context, buildArgs []string) error { pterm.Println(util.HighlightHCL(string(b))) return errors.Wrap(err, out) } + pterm.Success.Println("🔥 | baking containers ⏱︎ " + time.Since(start).Truncate(time.Second).String()) return nil @@ -448,12 +476,14 @@ func (sq *Squadron) Build(ctx context.Context, buildArgs []string, parallel int) unit string item config.Build } + var all []one gitInfo, err := sq.getGitInfo(ctx) if err != nil { pterm.Debug.Println("failed to get git info:", err) } + var gitInfoArgs []string for s, s2 := range gitInfo { gitInfoArgs = append(gitInfoArgs, fmt.Sprintf("%s=%s", s, s2)) @@ -477,6 +507,7 @@ func (sq *Squadron) Build(ctx context.Context, buildArgs []string, parallel int) }) spinner.Start() } + return nil }) }) @@ -500,6 +531,7 @@ func (sq *Squadron) Build(ctx context.Context, buildArgs []string, parallel int) } a.spinner.Success() + return nil }) } @@ -528,6 +560,7 @@ func (sq *Squadron) Down(ctx context.Context, helmArgs []string, parallel int) e } name := sq.getReleaseName(key, k, v) + namespace, err := sq.Namespace(ctx, key, k, v) if err != nil { return err @@ -546,8 +579,10 @@ func (sq *Squadron) Down(ctx context.Context, helmArgs []string, parallel int) e } spinner.Success() + return nil }) + return nil }) }) @@ -581,12 +616,17 @@ func (sq *Squadron) RenderSchema(ctx context.Context, baseSchema string) (string } func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) (string, error) { - var m sync.Mutex - var ret bytes.Buffer + var ( + m sync.Mutex + ret bytes.Buffer + ) + write := func(b []byte) error { m.Lock() defer m.Unlock() + _, err := ret.Write(b) + return err } @@ -610,10 +650,12 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) ( } name := sq.getReleaseName(key, k, v) + namespace, err := sq.Namespace(ctx, key, k, v) if err != nil { return err } + valueBytes, err := v.ValuesYAML(sq.c.Global) if err != nil { return err @@ -624,6 +666,7 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) ( spinner.Fail(string(manifest)) return err } + cmd := exec.CommandContext(ctx, "helm", "upgrade", name, "--install", "--namespace", namespace, @@ -643,11 +686,14 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) ( if v.Chart.Repository != "" { cmd.Args = append(cmd.Args, "--repo", v.Chart.Repository) } + if v.Chart.Version != "" { cmd.Args = append(cmd.Args, "--version", v.Chart.Version) } } + cmd.Args = append(cmd.Args, helmArgs...) + out, err := cmd.CombinedOutput() if err != nil { spinner.Fail(string(out)) @@ -661,6 +707,7 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) ( } outStr := strings.Split(string(out), "\n") + yamls2, err := yamldiff.Load(strings.Join(outStr[10:], "\n")) if err != nil { spinner.Fail(string(out)) @@ -678,6 +725,7 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) ( } spinner.Success() + return nil }) @@ -694,14 +742,17 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) ( func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int) error { var m sync.Mutex + tbd := pterm.TableData{ {"Name", "Revision", "Status", "User", "Branch", "Commit", "Squadron", "Last deployed", "Notes"}, } write := func(b []string) { m.Lock() defer m.Unlock() + tbd = append(tbd, b) } + type statusType struct { Name string `json:"name"` Version int `json:"version"` @@ -713,10 +764,10 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int) LastDeployed string `json:"last_deployed"` Description string `json:"description"` } `json:"info"` - user string `json:"-"` //nolint:revive - branch string `json:"-"` //nolint:revive - commit string `json:"-"` //nolint:revive - squadron string `json:"-"` //nolint:revive + user string `json:"-"` + branch string `json:"-"` + commit string `json:"-"` + squadron string `json:"-"` } wg, ctx := errgroup.WithContext(ctx) @@ -728,7 +779,9 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int) _ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error { return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error { var status statusType + name := sq.getReleaseName(key, k, v) + namespace, err := sq.Namespace(ctx, key, k, v) if err != nil { return errors.Errorf("failed to retrieve namsspace: %s/%s", key, k) @@ -766,8 +819,10 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int) return errors.Errorf("failed to retrieve status: %s/%s", key, k) } - var notes []string - var statusDescription Status + var ( + notes []string + statusDescription Status + ) if err := json.Unmarshal([]byte(status.Info.Description), &statusDescription); err == nil { status.user = statusDescription.User @@ -796,6 +851,7 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int) }) spinner.Success() + return nil }) @@ -806,12 +862,14 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int) if err := wg.Wait(); err != nil { return err } + printer.Stop() out, err := pterm.DefaultTable.WithHasHeader().WithData(tbd).Srender() if err != nil { return err } + pterm.Println(out) return nil @@ -831,6 +889,7 @@ func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []st _ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error { return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error { name := sq.getReleaseName(key, k, v) + namespace, err := sq.Namespace(ctx, key, k, v) if err != nil { return err @@ -848,6 +907,7 @@ func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []st } stdErr := bytes.NewBuffer([]byte{}) + out, err := util.NewHelmCommand().Args("rollback", name). Stderr(stdErr). Args(helmArgs...). @@ -863,6 +923,7 @@ func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []st } spinner.Success(out) + return nil }) @@ -878,11 +939,13 @@ func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []st func (sq *Squadron) UpdateLocalDependencies(ctx context.Context, parallel int) error { // collect unique entrie repositories := map[string]struct{}{} + err := sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error { return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error { if strings.HasPrefix(v.Chart.Repository, "file:///") { repositories[v.Chart.Repository] = struct{}{} } + return nil }) }) @@ -896,6 +959,7 @@ func (sq *Squadron) UpdateLocalDependencies(ctx context.Context, parallel int) e for repository := range repositories { wg.Go(func() error { pterm.Debug.Printfln("running helm dependency update for %s", repository) + if out, err := util.NewHelmCommand(). Cwd(path.Clean(strings.TrimPrefix(repository, "file://"))). Args("dependency", "update", "--skip-refresh", "--debug"). @@ -904,6 +968,7 @@ func (sq *Squadron) UpdateLocalDependencies(ctx context.Context, parallel int) e } else { pterm.Debug.Println(out) } + return nil }) } @@ -922,12 +987,14 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, status Status, pa printer := ptermx.MustNewMultiPrinter() defer printer.Stop() + type one struct { spinner ptermx.Spinner squadron string unit string item *config.Unit } + var all []one _ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error { @@ -936,6 +1003,7 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, status Status, pa if v.Priority != 0 { priority = fmt.Sprintf(" ☝︎ %d", v.Priority) } + spinner := printer.NewSpinner(fmt.Sprintf("🚀 | %s/%s", key, k) + priority) all = append(all, one{ spinner: spinner, @@ -964,11 +1032,13 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, status Status, pa } name := sq.getReleaseName(a.squadron, a.unit, a.item) + namespace, err := sq.Namespace(ctx, a.squadron, a.unit, a.item) if err != nil { a.spinner.Fail(err.Error()) return err } + valueBytes, err := a.item.ValuesYAML(sq.c.Global) if err != nil { a.spinner.Fail(err.Error()) @@ -993,9 +1063,11 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, status Status, pa cmd.Args(path.Clean(strings.TrimPrefix(a.item.Chart.Repository, "file://"))) } else { cmd.Args(a.item.Chart.Name) + if a.item.Chart.Repository != "" { cmd.Args("--repo", a.item.Chart.Repository) } + if a.item.Chart.Version != "" { cmd.Args("--version", a.item.Chart.Version) } @@ -1011,6 +1083,7 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, status Status, pa } a.spinner.Success() + return nil }) } @@ -1019,12 +1092,17 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, status Status, pa } func (sq *Squadron) Template(ctx context.Context, helmArgs []string, parallel int) (string, error) { - var m sync.Mutex - var ret bytes.Buffer + var ( + m sync.Mutex + ret bytes.Buffer + ) + write := func(b []byte) error { m.Lock() defer m.Unlock() + _, err := ret.Write(b) + return err } @@ -1048,6 +1126,7 @@ func (sq *Squadron) Template(ctx context.Context, helmArgs []string, parallel in } name := sq.getReleaseName(key, k, v) + namespace, err := sq.Namespace(ctx, key, k, v) if err != nil { spinner.Fail(err.Error()) @@ -1066,6 +1145,7 @@ func (sq *Squadron) Template(ctx context.Context, helmArgs []string, parallel in } spinner.Success() + return nil }) @@ -1084,6 +1164,7 @@ func (sq *Squadron) getReleaseName(squadron, unit string, u *config.Unit) string if u.Name != "" { return u.Name } + return squadron + "-" + unit } @@ -1091,6 +1172,7 @@ func (sq *Squadron) getGitInfo(ctx context.Context) (map[string]string, error) { ret := map[string]string{} dir := "." + for _, s := range []string{"GIT_DIR", "PROJECT_ROOT"} { if v := os.Getenv(s); v != "" { dir = v @@ -1124,6 +1206,7 @@ func (sq *Squadron) getGitInfo(ctx context.Context) (map[string]string, error) { } } } + ret["GIT_REPOSITORY_URL"] = url } @@ -1142,10 +1225,13 @@ func (sq *Squadron) getGitInfo(ctx context.Context) (map[string]string, error) { if ref.Hash() == reference.Hash() { ret["GIT_TAG"] = reference.Name().Short() ret["GIT_TYPE"] = "tag" + return errors.New("break") } + return nil }) } + return ret, nil } diff --git a/squadron_test.go b/squadron_test.go index 399a25e..a6c69b9 100644 --- a/squadron_test.go +++ b/squadron_test.go @@ -70,13 +70,16 @@ func TestConfigSimpleSnapshot(t *testing.T) { func runTestConfig(t *testing.T, name string, files []string, squadronName string, unitNames, tags []string) { t.Helper() + var cwd string + ctx := t.Context() require.NoError(t, util.ValidatePath(".", &cwd)) for i, file := range files { files[i] = path.Join("testdata", name, file) } + sq := squadron.New(cwd, "default", files) {