Merge pull request #116 from foomo/feature/default-build-args
Some checks failed
Test Branch / test (push) Has been cancelled

feat: default build args
This commit is contained in:
Kevin Franklin Kim 2025-10-10 14:29:09 +02:00 committed by GitHub
commit 470cb96905
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 572 additions and 249 deletions

View File

@ -9,7 +9,6 @@ on:
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@ -28,9 +27,13 @@ jobs:
check-latest: true
go-version-file: go.mod
- uses: golangci/golangci-lint-action@v8
- name: Setup golangci-lint cache
uses: actions/cache@v4
with:
install-mode: none
path: ~/.cache/golangci-lint
key: ${{ runner.os }}-golangci-lint-${{ hashFiles('**/go.mod') }}
restore-keys: |
${{ runner.os }}-golangci-lint-
- name: Run lint
run: make lint

View File

@ -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$

View File

@ -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"

View File

@ -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:

View File

@ -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")

View File

@ -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")

View File

@ -64,6 +64,7 @@ PowerShell:
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
return nil
},
}

View File

@ -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

View File

@ -46,6 +46,7 @@ func NewDiff(c *viper.Viper) *cobra.Command {
if !x.GetBool("raw") {
out = util.Highlight(out)
}
pterm.Println(out)
return nil

View File

@ -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()
}

View File

@ -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()
},
}

View File

@ -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
}

View File

@ -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

View File

@ -47,6 +47,7 @@ func NewTemplate(c *viper.Viper) *cobra.Command {
if !x.GetBool("raw") {
out = util.Highlight(out)
}
pterm.Println(out)
return nil

View File

@ -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
})
}

View File

@ -16,5 +16,6 @@ func NewVersion(c *viper.Viper) *cobra.Command {
pterm.Println(version)
},
}
return cmd
}

13
go.mod
View File

@ -13,7 +13,7 @@ require (
github.com/alecthomas/chroma v0.10.0
github.com/foomo/go v0.0.3
github.com/genelet/determined v1.12.3
github.com/go-git/go-git/v5 v5.16.2
github.com/go-git/go-git/v5 v5.16.3
github.com/invopop/jsonschema v0.13.0
github.com/miracl/conflate v1.3.4
github.com/pkg/errors v0.9.1
@ -44,9 +44,10 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/cyphar/filepath-securejoin v0.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
@ -73,7 +74,7 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
@ -85,10 +86,10 @@ require (
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
@ -110,7 +111,7 @@ require (
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect

14
go.sum
View File

@ -58,6 +58,8 @@ github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQ
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
@ -66,6 +68,8 @@ github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -99,6 +103,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8=
github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
@ -161,6 +167,8 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@ -207,6 +215,8 @@ github.com/runz0rd/conflate v1.2.2-0.20210920145208-fa48576ef06d/go.mod h1:F85f+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
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/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
@ -215,6 +225,8 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@ -303,6 +315,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -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{}
}

View File

@ -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).
@ -114,7 +118,7 @@ func (b *Build) Build(ctx context.Context, squadron, unit string, args []string)
ListArg("--allow", b.Allow).
ListArg("--attest", b.Attest).
ListArg("--build-arg", b.BuildArg).
ListArg("--build-contet", b.BuildContext).
ListArg("--build-context", b.BuildContext).
Arg(argOverride("--builder", b.Builder, args)).
Arg(argOverride("--cache-from", b.CacheFrom, args)).
Arg(argOverride("--cache-to", b.CacheTo, args)).
@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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 ")
}

View File

@ -15,6 +15,7 @@ func NewStandardMultiPrinter() (*StandardMultiPrinter, error) {
if err != nil {
return nil, err
}
return &StandardMultiPrinter{printer: printer}, nil
}

View File

@ -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 ")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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, " ")
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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...)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -5,6 +5,8 @@ import (
"context"
"encoding/json"
"fmt"
"maps"
"os"
"os/exec"
"path"
"slices"
@ -19,6 +21,8 @@ import (
templatex "github.com/foomo/squadron/internal/template"
"github.com/foomo/squadron/internal/util"
"github.com/genelet/determined/dethcl"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/miracl/conflate"
"github.com/pkg/errors"
"github.com/pterm/pterm"
@ -55,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
@ -63,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})
}
@ -80,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")
@ -95,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 + "'")
@ -111,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
}
@ -160,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")
}
@ -173,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)
}
@ -202,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)
}
@ -241,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 {
@ -257,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 {
@ -271,6 +291,7 @@ func (sq *Squadron) Push(ctx context.Context, pushArgs []string, parallel int) e
spinner.Start()
}
}
return nil
})
})
@ -293,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
})
}
@ -330,6 +354,7 @@ func (sq *Squadron) BuildDependencies(ctx context.Context, buildArgs []string, p
}
spinner.Success()
return nil
}
@ -382,19 +407,30 @@ func (sq *Squadron) Bake(ctx context.Context, buildArgs []string) error {
Name: "all",
}
gitInfo, err := sq.getGitInfo(ctx)
if err != nil {
pterm.Debug.Println("failed to get git info:", err)
}
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
_ = value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
for _, name := range v.BakeNames() {
item := v.Bakes[name]
item.Name = strings.Join([]string{key, k, name}, "-")
item.Args["SQUADRON_NAME"] = key
item.Args["SQUADRON_UNIT_NAME"] = k
maps.Copy(item.Args, gitInfo)
pterm.Info.Printfln("📦 | %s/%s.%s (%s)", key, k, name, strings.Join(item.Tags, ","))
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)
@ -402,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)).
@ -414,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
@ -436,12 +476,28 @@ 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))
}
_ = 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 {
for _, name := range v.BuildNames() {
item := v.Builds[name]
item.BuildArg = append(item.BuildArg,
"SQUADRON_NAME="+key,
"SQUADRON_UNIT_NAME="+k,
)
item.BuildArg = append(item.BuildArg, gitInfoArgs...)
spinner := printer.NewSpinner(fmt.Sprintf("📦 | %s/%s.%s (%s:%s)", key, k, name, item.Image, item.Tag))
all = append(all, one{
spinner: spinner,
@ -451,6 +507,7 @@ func (sq *Squadron) Build(ctx context.Context, buildArgs []string, parallel int)
})
spinner.Start()
}
return nil
})
})
@ -462,17 +519,19 @@ func (sq *Squadron) Build(ctx context.Context, buildArgs []string, parallel int)
ctx := ptermx.ContextWithSpinner(ctx, a.spinner)
if err := ctx.Err(); err != nil {
a.spinner.Warning(err.Error())
return err
return nil
}
if out, err := a.item.Build(ctx, a.squadron, a.unit, buildArgs); err != nil {
if !errors.Is(err, context.Canceled) {
a.spinner.Fail(out)
}
if out, err := a.item.Build(ctx, a.squadron, a.unit, buildArgs); errors.Is(ctx.Err(), context.Canceled) {
a.spinner.Warning(ctx.Err().Error())
return nil
} else if err != nil {
a.spinner.Fail(out)
return err
}
a.spinner.Success()
return nil
})
}
@ -501,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
@ -519,8 +579,10 @@ func (sq *Squadron) Down(ctx context.Context, helmArgs []string, parallel int) e
}
spinner.Success()
return nil
})
return nil
})
})
@ -554,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
}
@ -583,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
@ -597,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,
@ -616,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))
@ -634,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))
@ -651,6 +725,7 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) (
}
spinner.Success()
return nil
})
@ -667,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"`
@ -686,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)
@ -701,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)
@ -739,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
@ -769,6 +851,7 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int)
})
spinner.Success()
return nil
})
@ -779,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
@ -804,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
@ -821,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...).
@ -836,6 +923,7 @@ func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []st
}
spinner.Success(out)
return nil
})
@ -851,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
})
})
@ -869,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").
@ -877,6 +968,7 @@ func (sq *Squadron) UpdateLocalDependencies(ctx context.Context, parallel int) e
} else {
pterm.Debug.Println(out)
}
return nil
})
}
@ -895,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 {
@ -909,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,
@ -937,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())
@ -966,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)
}
@ -984,6 +1083,7 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, status Status, pa
}
a.spinner.Success()
return nil
})
}
@ -992,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
}
@ -1021,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())
@ -1039,6 +1145,7 @@ func (sq *Squadron) Template(ctx context.Context, helmArgs []string, parallel in
}
spinner.Success()
return nil
})
@ -1057,5 +1164,74 @@ func (sq *Squadron) getReleaseName(squadron, unit string, u *config.Unit) string
if u.Name != "" {
return u.Name
}
return squadron + "-" + unit
}
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
break
}
}
repo, err := git.PlainOpen(dir)
if err != nil {
return nil, err
}
// Get remote "origin" URL
remote, err := repo.Remote("origin")
if err != nil {
return nil, err
}
remoteURLs := remote.Config().URLs
if len(remoteURLs) > 0 {
url := remoteURLs[0]
if strings.HasPrefix(url, "git@") {
// Example input: git@github.com:user/repo.git
parts := strings.SplitN(url, ":", 2)
if len(parts) == 2 {
// parts[1] = user/repo.git
hostParts := strings.Split(parts[0], "@")
if len(hostParts) == 2 {
host := hostParts[1]
url = "https://" + host + "/" + strings.TrimSuffix(parts[1], ".git")
}
}
}
ret["GIT_REPOSITORY_URL"] = url
}
// Get HEAD reference to find the current branch or commit
ref, err := repo.Head()
if err != nil {
return nil, err
}
ret["GIT_TYPE"] = "branch"
ret["GIT_BRANCH"] = ref.Name().Short()
ret["GIT_COMMIT_HASH"] = ref.Hash().String()
if t, err := repo.Tags(); err == nil {
_ = t.ForEach(func(reference *plumbing.Reference) 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
}

View File

@ -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)
{