From aa9474b9b1d72cbedfe294a423700cae6254420f Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Thu, 25 Sep 2025 12:42:10 +0200 Subject: [PATCH 1/9] Implement new keyring template processor. --- .golangci.yaml | 93 ++++++++++----------- Makefile | 167 +++++++++++++++++++++++++++++++++---- askpass.go | 36 +++++++- file.go | 24 +++++- go.mod | 102 +++++++++++++---------- go.sum | 219 ++++++++++++++++++++++++++++--------------------- keyring.go | 37 +++++---- plugin.go | 148 +++++++++++++++++++++++---------- plugin_test.go | 87 ++++++++++++++++++-- yaml.go | 4 +- 10 files changed, 639 insertions(+), 278 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 7519d73..a8b9b80 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,55 +1,56 @@ -# More info on config here: https://github.com/golangci/golangci-lint#config-file -run: - deadline: 10s - issues-exit-code: 1 - tests: true - +# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json +version: "2" output: formats: - - format: colored-line-number - print-issued-lines: true - print-linter-name: true - -linters-settings: - govet: - shadow: true - golint: - min-confidence: 0 - dupl: - threshold: 100 - goconst: - min-len: 2 - min-occurrences: 2 - + text: + path: stdout + print-linter-name: true + print-issued-lines: true linters: - disable-all: true + default: none enable: - - revive - - govet - - errcheck - - unused - - ineffassign - - typecheck - dupl + - errcheck - goconst - gosec - - goimports - - gosimple + - govet + - ineffassign + - revive - staticcheck - unused - -issues: - exclude-use-default: false - exclude-dirs: - - bin - - vendor - - var - - tmp - exclude-files: - - \.pb\.go$ - - \.pb\.goclay\.go$ - exclude: -# # _ instead of err checks -# - G104 - # errcheck: Almost all programs ignore errors on these functions and in most cases it's ok - - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv|.*Rollback). is not checked + settings: + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + exclusions: + generated: lax + rules: + - path: (.+)\.go$ + text: Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv|.*Rollback). is not checked + paths: + - \.pb\.go$ + - \.pb\.goclay\.go$ + - bin + - vendor + - var + - tmp + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - goimports + exclusions: + generated: lax + paths: + - \.pb\.go$ + - \.pb\.goclay\.go$ + - bin + - vendor + - var + - tmp + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index 2e9a3cb..44656ab 100644 --- a/Makefile +++ b/Makefile @@ -23,53 +23,186 @@ LOCAL_BIN:=$(CURDIR)/bin # Linter config. GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint -GOLANGCI_TAG:=1.64.5 +GOLANGCI_TAG:=2.3.0 + +GOTESTFMT_BIN:=$(GOBIN)/gotestfmt + +# Color definitions +RED=\033[0;31m +GREEN=\033[0;32m +YELLOW=\033[0;33m +BLUE=\033[0;34m +MAGENTA=\033[0;35m +CYAN=\033[0;36m +WHITE=\033[0;37m +BOLD=\033[1m +RESET=\033[0m + +# Disable colors on Windows. +ifeq ($(OS),Windows_NT) + RED= + GREEN= + YELLOW= + BLUE= + MAGENTA= + CYAN= + WHITE= + BOLD= + RESET= +endif + +# Print functions +define print_header + @echo "$(BOLD)$(CYAN)╔═════════════════════════════════════════════════════════════╗$(RESET)" + @echo "$(BOLD)$(CYAN)║ LAUNCHR ║$(RESET)" + @echo "$(BOLD)$(CYAN)╚═════════════════════════════════════════════════════════════╝$(RESET)" +endef + +define print_success + @echo "$(BOLD)$(GREEN)✅ $(1)$(RESET)" + @echo +endef + +define print_info + @echo "$(BOLD)$(BLUE)📋 $(1)$(RESET)" + @echo +endef + +define print_warning + @echo "$(BOLD)$(YELLOW)⚠️ $(1)$(RESET)" + @echo +endef + +define print_error + @echo "$(BOLD)$(RED)❌ $(1)$(RESET)" + @echo +endef + +define print_step + @echo "$(BOLD)$(MAGENTA)🔧 $(1)$(RESET)" +endef .PHONY: all -all: deps test build +all: banner deps test-short build + $(call print_success,"🎉 All tasks completed successfully!") + +.PHONY: banner +banner: + $(call print_header) + @echo "$(BOLD)$(WHITE)📦 Version: $(APP_VERSION)$(RESET)" + @echo "$(BOLD)$(WHITE)🌿 Branch: $(GIT_BRANCH)$(RESET)" + @echo "$(BOLD)$(WHITE)🔗 Hash: $(GIT_HASH)$(RESET)" + @echo # Install go dependencies .PHONY: deps deps: - $(info Installing go dependencies...) - go mod download + $(call print_step,"Installing go dependencies...") + @go mod download + $(call print_success,"Dependencies installed successfully!") # Run all tests .PHONY: test -test: - $(info Running tests...) - go test ./... +test: .install-gotestfmt + $(call print_step,"Running all tests...") + @go test -json -v ./... | $(GOTESTFMT_BIN) -hide all && \ + echo "$(BOLD)$(GREEN)🧪 ✅ All tests passed$(RESET)" || \ + echo "$(BOLD)$(RED)🧪 ❌ Some tests failed$(RESET)" + @echo + +# Run short tests +.PHONY: test-short +test-short: .install-gotestfmt + $(call print_step,"Running short tests...") + @go test -json -short -v ./... | $(GOTESTFMT_BIN) -hide all && \ + echo "$(BOLD)$(GREEN)🧪 ✅ All short tests passed$(RESET)" || \ + echo "$(BOLD)$(RED)🧪 ❌ Some short tests failed$(RESET)" + @echo # Build launchr .PHONY: build build: - $(info Building launchr...) + $(call print_step,"Building launchr...") # Application related information available on build time. $(eval LDFLAGS:=-X '$(GOPKG).name=launchr' -X '$(GOPKG).version=$(APP_VERSION)' $(LDFLAGS_EXTRA)) $(eval BIN?=$(LOCAL_BIN)/launchr) - go generate ./... - $(BUILD_ENVPARMS) go build -ldflags "$(LDFLAGS)" $(BUILD_OPTS) -o $(BIN) ./cmd/launchr + @go generate ./... + @$(BUILD_ENVPARMS) go build -ldflags "$(LDFLAGS)" $(BUILD_OPTS) -o $(BIN) ./cmd/launchr + $(call print_success,"🔨 Build completed: $(BIN)") # Install launchr .PHONY: install install: all -install: - $(info Installing launchr to GOPATH...) - cp $(LOCAL_BIN)/launchr $(GOBIN)/launchr + $(call print_step,"Installing launchr to GOPATH...") + @cp $(LOCAL_BIN)/launchr $(GOBIN)/launchr + $(call print_success,"🚀 launchr installed to $(GOBIN)/launchr") # Install and run linters .PHONY: lint -lint: .install-lint .lint +lint: .install-lint .lint-fix # Install golangci-lint binary .PHONY: .install-lint .install-lint: ifeq ($(wildcard $(GOLANGCI_BIN)),) - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LOCAL_BIN) v$(GOLANGCI_TAG) + $(call print_step,"Installing golangci-lint v$(GOLANGCI_TAG)...") + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LOCAL_BIN) v$(GOLANGCI_TAG) + $(call print_success,"golangci-lint installed!") +endif + +# Install gotestfmt binary +.PHONY: .install-gotestfmt +.install-gotestfmt: +ifeq ($(wildcard $(GOTESTFMT_BIN)),) + $(call print_step,"Installing gotestfmt...") + @go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest + $(call print_success,"gotestfmt installed!") endif # Runs linters +.PHONY: .lint-fix +.lint-fix: + $(call print_step,"Running linters with auto-fix...") + @$(GOLANGCI_BIN) run --fix ./... && \ + echo "$(BOLD)$(GREEN)🔍 ✅ All linting checks passed$(RESET)" || \ + echo "$(BOLD)$(YELLOW)🔍 ⚠️ Some linting issues found - please review$(RESET)" + @echo + .PHONY: .lint .lint: - $(info Running lint...) - $(GOLANGCI_BIN) run --fix ./... + $(call print_step,"Running linters...") + @$(GOLANGCI_BIN) run && \ + echo "$(BOLD)$(GREEN)🔍 ✅ All linting checks passed$(RESET)" || \ + echo "$(BOLD)$(YELLOW)🔍 ⚠️ Some linting issues found - please review$(RESET)" + @echo + +# Clean build artifacts +.PHONY: clean +clean: + $(call print_step,"Cleaning build artifacts...") + @rm -rf $(LOCAL_BIN) + $(call print_success,"🧹 Cleanup completed!") + +# Show help +.PHONY: help +help: + $(call print_header) + @echo "$(BOLD)$(WHITE)Available targets:$(RESET)" + @echo "" + @echo " $(BOLD)$(GREEN)all$(RESET) 🎯 Run deps, test, and build" + @echo " $(BOLD)$(GREEN)deps$(RESET) 📦 Install go dependencies" + @echo " $(BOLD)$(GREEN)test$(RESET) 🧪 Run all tests" + @echo " $(BOLD)$(GREEN)test-short$(RESET) ⚡ Run short tests only" + @echo " $(BOLD)$(GREEN)build$(RESET) 🔨 Build launchr binary" + @echo " $(BOLD)$(GREEN)install$(RESET) 🚀 Install launchr to GOPATH" + @echo " $(BOLD)$(GREEN)lint$(RESET) 🔍 Run linters with auto-fix" + @echo " $(BOLD)$(GREEN)clean$(RESET) 🧹 Clean build artifacts" + @echo " $(BOLD)$(GREEN)help$(RESET) ❓ Show this help message" + @echo "" + @echo "$(BOLD)$(CYAN)Environment variables:$(RESET)" + @echo " $(BOLD)$(YELLOW)DEBUG=1$(RESET) Enable debug build" + @echo " $(BOLD)$(YELLOW)BIN=path$(RESET) Custom binary output path" + @echo "" + +# Default target shows help +.DEFAULT_GOAL := help diff --git a/askpass.go b/askpass.go index 953da78..42a3138 100644 --- a/askpass.go +++ b/askpass.go @@ -79,11 +79,39 @@ func (a AskPassWithTerminal) readPass(prompt string) (string, error) { return strings.TrimSpace(string(bytePassword)), nil } -// AskPassConstFlow implements AskPass and returns constant. -type AskPassConstFlow string +// AskPassConst implements AskPass and returns constant. +type AskPassConst func() string // GetPass implements AskPass interface. -func (a AskPassConstFlow) GetPass() (string, error) { return string(a), nil } +func (a AskPassConst) GetPass() (string, error) { return a(), nil } // NewPass implements AskPass interface. -func (a AskPassConstFlow) NewPass() (string, error) { return string(a), nil } +func (a AskPassConst) NewPass() (string, error) { return a(), nil } + +type AskPassFirstAvailable []AskPass + +func (a AskPassFirstAvailable) GetPass() (string, error) { + for _, a := range a { + pass, err := a.GetPass() + if err != nil { + return "", err + } + if pass != "" { + return pass, nil + } + } + return "", nil +} + +func (a AskPassFirstAvailable) NewPass() (string, error) { + for _, a := range a { + pass, err := a.NewPass() + if err != nil { + return "", err + } + if pass != "" { + return pass, nil + } + } + return "", nil +} diff --git a/file.go b/file.go index a18b6b7..1d8ce04 100644 --- a/file.go +++ b/file.go @@ -18,18 +18,34 @@ type CredentialsFile interface { // See os.OpenFile for more info about flag and perm arguments. Open(flag int, perm os.FileMode) error // Unlock decrypts a file if supported. - Unlock(bool) error + Unlock(askNew bool) error // Lock makes it to request Unlock again. Lock() // Remove deletes a file from FS. Remove() error } +type nullFile struct{} + +func (_ nullFile) Open(_ int, _ os.FileMode) (err error) { return nil } +func (_ nullFile) Unlock(_ bool) error { return nil } +func (_ nullFile) Lock() {} +func (_ nullFile) Read(_ []byte) (int, error) { return 0, io.EOF } +func (_ nullFile) Write(p []byte) (int, error) { return len(p), nil } +func (_ nullFile) Close() error { return nil } +func (_ nullFile) Remove() error { return nil } + type plainFile struct { fname string file io.ReadWriteCloser } +func NewPlainFile(fname string) CredentialsFile { + return &plainFile{ + fname: fname + ".age", + } +} + func (f *plainFile) Open(flag int, perm os.FileMode) (err error) { isCreate := flag&os.O_CREATE == os.O_CREATE if isCreate { @@ -69,7 +85,7 @@ type ageFile struct { w io.WriteCloser } -func newAgeFile(fname string, askPass AskPass) CredentialsFile { +func NewAgeFile(fname string, askPass AskPass) CredentialsFile { return &ageFile{ file: &plainFile{ fname: fname + ".age", @@ -82,11 +98,11 @@ func (f *ageFile) Open(flag int, perm os.FileMode) (err error) { return f.file.O func (f *ageFile) Remove() error { return f.file.Remove() } func (f *ageFile) Lock() { f.passphrase = "" } -func (f *ageFile) Unlock(pass bool) (err error) { +func (f *ageFile) Unlock(askNew bool) (err error) { if f.passphrase != "" { return nil } - if pass { + if askNew { f.passphrase, err = f.askPass.NewPass() } else { f.passphrase, err = f.askPass.GetPass() diff --git a/go.mod b/go.mod index c3e4ff7..fdf1419 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,9 @@ toolchain go1.24.1 require ( filippo.io/age v1.2.1 github.com/launchrctl/launchr v0.21.2 - github.com/stretchr/testify v1.10.0 - golang.org/x/term v0.31.0 + github.com/rogpeppe/go-internal v1.14.1 + github.com/stretchr/testify v1.11.1 + golang.org/x/term v0.35.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -19,26 +20,38 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/containerd/console v1.0.4 // indirect + github.com/containerd/console v1.0.5 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.1.1+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/docker v28.4.0+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gookit/color v1.5.4 // indirect + github.com/gookit/color v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -46,7 +59,7 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/knadh/koanf v1.5.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -61,7 +74,7 @@ require ( github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect @@ -69,43 +82,46 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/pterm/pterm v0.12.80 // indirect + github.com/pterm/pterm v0.12.81 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/mock v0.5.1 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/time v0.11.0 // indirect + go.uber.org/mock v0.6.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.13.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/api v0.33.0 // indirect - k8s.io/apimachinery v0.33.0 // indirect - k8s.io/client-go v0.33.0 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apimachinery v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 75668d8..228a15c 100644 --- a/go.sum +++ b/go.sum @@ -63,8 +63,12 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= -github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -80,15 +84,15 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= -github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= +github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -101,8 +105,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -112,16 +116,38 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -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-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -148,8 +174,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -159,7 +185,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -168,10 +193,12 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAx github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= +github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= +github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -253,12 +280,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/launchrctl/launchr v0.21.2 h1:D53UHpal9+/Qf+2di5+WS3/9RMWPOJMqeWVtv/gqesI= github.com/launchrctl/launchr v0.21.2/go.mod h1:C7H4FHMSjNi4fUt36rzfyE/2xSpdBdUuq7n7IPAzqEo= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -314,8 +340,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -370,21 +397,21 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= -github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= +github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA= +github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -394,11 +421,12 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -409,8 +437,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -423,39 +451,43 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= -go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= @@ -469,8 +501,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -490,13 +522,13 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -547,15 +579,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -565,11 +597,11 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -583,8 +615,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -622,16 +654,16 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -651,25 +683,24 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= -k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= -k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= -k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= -k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/keyring.go b/keyring.go index 8ae45db..d380a57 100644 --- a/keyring.go +++ b/keyring.go @@ -7,6 +7,14 @@ import ( "github.com/launchrctl/launchr" ) +type StoreType string + +const ( + StoreTypeInMemoryYaml StoreType = "inmemoryYaml" + StoreTypePlainYamlFile StoreType = "plainYaml" + StoreTypeAgeYamlFile StoreType = "ageYaml" +) + const defaultFileYaml = "keyring.yaml" // Keyring errors. @@ -110,20 +118,24 @@ type Keyring interface { } type keyringService struct { - fname string store DataStore - cfg launchr.Config mask *launchr.SensitiveMask } -func newKeyringService(cfg launchr.Config, mask *launchr.SensitiveMask) Keyring { +func NewService(store DataStore, mask *launchr.SensitiveMask) Keyring { return &keyringService{ - fname: cfg.Path(defaultFileYaml), - cfg: cfg, + store: store, mask: mask, } } +func NewFileStore(f CredentialsFile) DataStore { + if f == nil { + f = nullFile{} + } + return &dataStoreYaml{file: f} +} + // ServiceInfo implements [launchr.Service] interface. func (k *keyringService) ServiceInfo() launchr.ServiceInfo { return launchr.ServiceInfo{} @@ -135,18 +147,6 @@ func (k *keyringService) ResetStorage() { } func (k *keyringService) defaultStore() (DataStore, error) { - if k.store != nil { - return k.store, nil - } - var askPass AskPass - if passphrase != "" { - askPass = AskPassConstFlow(passphrase) - } else { - askPass = AskPassWithTerminal{} - } - // @todo parse header to know if it's encrypted or not. - // @todo do not encrypt if the passphrase is not provided. - k.store = &dataStoreYaml{file: newAgeFile(k.fname, askPass)} return k.store, nil } @@ -209,6 +209,9 @@ func (k *keyringService) AddItem(item SecretItem) error { // MaskItem masks the item values func (k *keyringService) maskItem(item SecretItem) { + if k.mask == nil { + return + } switch dataItem := item.(type) { case CredentialsItem: k.mask.AddString(dataItem.Password) diff --git a/plugin.go b/plugin.go index c3f40a7..8ff63ee 100644 --- a/plugin.go +++ b/plugin.go @@ -19,18 +19,18 @@ import ( const ( procGetKeyValue = "keyring.GetKeyValue" - errTplNotFoundURL = "%s not found in keyring. Use `%s keyring:login` to add it." - errTplNotFoundKey = "%s not found in keyring. Use `%s keyring:set` to add it." + errTplNotFoundURL = "%q not found in keyring. Use `%s keyring:login %s` to add it." + errTplNotFoundKey = "%q not found in keyring. Use `%s keyring:set %s` to add it." envVarPassphrase = launchr.EnvVar("keyring_passphrase") envVarPassphraseFile = launchr.EnvVar("keyring_passphrase_file") -) -var ( - passphrase string - passphraseFile string + paramPassphrase = "keyring-passphrase" + paramPassphraseFile = "keyring-passphrase-file" ) +var passphrase = &persistentPassphrase{} + var ( //go:embed action.list.yaml actionListYaml []byte @@ -52,9 +52,7 @@ func init() { // Plugin is [launchr.Plugin] plugin providing a keyring. type Plugin struct { - k Keyring - cfg launchr.Config - mask *launchr.SensitiveMask + k Keyring } // PluginInfo implements [launchr.Plugin] interface. @@ -64,15 +62,31 @@ func (p *Plugin) PluginInfo() launchr.PluginInfo { // OnAppInit implements [launchr.Plugin] interface. func (p *Plugin) OnAppInit(app launchr.App) error { - p.mask = app.SensitiveMask() - app.GetService(&p.cfg) - p.k = newKeyringService(p.cfg, p.mask) - app.AddService(p.k) + mask := app.SensitiveMask() + var cfg launchr.Config + var tp action.TemplateProcessors + app.Services().Get(&cfg) + app.Services().Get(&tp) + + // Read keyring from a global config directory. + // TODO: parse header to know if it's encrypted or not. + // TODO: do not encrypt if the passphrase is not provided. + passphrase.mask = app.SensitiveMask() + store := NewFileStore( + NewAgeFile( + cfg.Path(defaultFileYaml), + AskPassFirstAvailable{ + AskPassConst(passphrase.get), + AskPassWithTerminal{}, + }, + ), + ) + + p.k = NewService(store, mask) + app.Services().Add(p.k) + + addTemplateProcessors(tp, p.k) - var m action.Manager - app.GetService(&m) - - addValueProcessors(m, p.k) return nil } @@ -81,14 +95,20 @@ type GetKeyValueProcessorOptions = *action.GenericValueProcessorOptions[struct { Key string `yaml:"key" validate:"not-empty"` }] -// addValueProcessors adds a keyring [action.ValueProcessor] to [action.Manager]. -func addValueProcessors(m action.Manager, keyring Keyring) { - m.AddValueProcessor(procGetKeyValue, action.GenericValueProcessor[GetKeyValueProcessorOptions]{ +// addTemplateProcessors adds keyring [action.TemplateProcessors]. +func addTemplateProcessors(tp action.TemplateProcessors, keyring Keyring) { + tp.AddValueProcessor(procGetKeyValue, action.GenericValueProcessor[GetKeyValueProcessorOptions]{ Types: []jsonschema.Type{jsonschema.String}, Fn: func(v any, opts GetKeyValueProcessorOptions, ctx action.ValueProcessorContext) (any, error) { return processGetByKey(v, opts, ctx, keyring) }, }) + + tp.AddTemplateFunc("keyring", func(ctx action.TemplateFuncContext) any { + return func() *keyringTemplateFunc { + return &keyringTemplateFunc{k: keyring} + } + }) } func processGetByKey(value any, opts GetKeyValueProcessorOptions, ctx action.ValueProcessorContext, k Keyring) (any, error) { @@ -131,6 +151,25 @@ func processGetByKey(value any, opts GetKeyValueProcessorOptions, ctx action.Val return value, buildNotFoundError(opts.Fields.Key, errTplNotFoundKey, err) } +// keyringTemplateFunc is a set of template functions to interact with [Keyring] in [action.TemplateProcessors]. +type keyringTemplateFunc struct { + k Keyring +} + +// Get returns a keyring key-value by a key. +// +// Usage: +// +// {{ keyring.Get "foo-bar" }} - retrieves value of any type. +func (t *keyringTemplateFunc) Get(key string) (any, error) { + v, err := t.k.GetForKey(key) + if err == nil { + return v.Value, nil + } + + return "", buildNotFoundError(key, errTplNotFoundKey, err) +} + // DiscoverActions implements [launchr.ActionDiscoveryPlugin] interface. func (p *Plugin) DiscoverActions(_ context.Context) ([]*action.Action, error) { // Action list. @@ -209,21 +248,15 @@ func (p *Plugin) DiscoverActions(_ context.Context) ([]*action.Action, error) { // CobraAddCommands implements [launchr.CobraPlugin] interface to provide keyring functionality. func (p *Plugin) CobraAddCommands(rootCmd *launchr.Command) error { - rootCmd.PersistentFlags().StringVarP(&passphrase, "keyring-passphrase", "", "", "Passphrase for keyring encryption/decryption") - rootCmd.PersistentFlags().StringVarP(&passphraseFile, "keyring-passphrase-file", "", "", "File containing passphrase for keyring encryption/decryption") + rootCmd.PersistentFlags().StringVarP(&passphrase.pass, paramPassphrase, "", "", "Passphrase for keyring encryption/decryption") + rootCmd.PersistentFlags().StringVarP(&passphrase.file, paramPassphraseFile, "", "", "File containing passphrase for keyring encryption/decryption") return nil } // PersistentPreRun implements [launchr.PersistentPreRun] interface. -func (p *Plugin) PersistentPreRun(_ *launchr.Command, _ []string) error { - setPassphrase() - // If the passphrase is set with env variables, hide them. - if passphraseFile == "" && passphrase != "" { - p.mask.AddString(passphrase) - } - if passphraseFile != "" { - p.mask.AddString(passphraseFile) - } +func (p *Plugin) PersistentPreRun(cmd *launchr.Command, _ []string) error { + passphrase.changedPass = cmd.Flags().Changed("keyring-passphrase") + passphrase.changedFile = cmd.Flags().Changed("keyring-passphrase-file") return nil } @@ -233,7 +266,7 @@ func buildNotFoundError(item, template string, err error) error { } version := launchr.Version() - return fmt.Errorf(template, item, version.Name) + return fmt.Errorf(template, item, version.Name, item) } func list(k Keyring, printer *launchr.Terminal) error { @@ -411,32 +444,59 @@ func purge(k Keyring) error { return k.Destroy() } -func setPassphrase() { +type persistentPassphrase struct { + pass string + file string + mask *launchr.SensitiveMask + + initialized bool + changedPass bool + changedFile bool +} + +func (p *persistentPassphrase) init() { + if p.initialized { + return + } + + defer func() { + // If the passphrase is set with user input or env variable, hide it. + if p.file == "" && p.pass != "" { + p.mask.AddString(p.pass) + } + p.initialized = true + }() + // Return passphrase if it's already provided. - if passphrase != "" { + if p.changedPass || p.pass != "" { return } // Check env variable for the passphrase. - passphrase = envVarPassphrase.Get() - if passphrase != "" { + p.pass = envVarPassphrase.Get() + if p.pass != "" { return } - if envPassFile := envVarPassphraseFile.Get(); passphraseFile == "" && envPassFile != "" { - passphraseFile = launchr.MustAbs(envPassFile) + if envPassFile := envVarPassphraseFile.Get(); p.file == "" && envPassFile != "" { + p.file = launchr.MustAbs(envPassFile) // Override to absolute path. - _ = envVarPassphraseFile.Set(passphraseFile) + _ = envVarPassphraseFile.Set(p.file) } // Try to read a secret from a file. - if passphraseFile != "" { - passphraseFile = launchr.MustAbs(passphraseFile) - bytes, err := os.ReadFile(passphraseFile) //nolint:gosec // Filepath checked on previous line. + if p.file != "" { + p.file = launchr.MustAbs(p.file) + bytes, err := os.ReadFile(p.file) //nolint:gosec // Filepath checked on previous line. if err != nil { return } - passphrase = strings.TrimSpace(string(bytes)) + p.pass = strings.TrimSpace(string(bytes)) // Set env variable for subprocesses. - _ = envVarPassphraseFile.Set(passphraseFile) + _ = envVarPassphraseFile.Set(p.file) return } } + +func (p *persistentPassphrase) get() string { + p.init() + return p.pass +} diff --git a/plugin_test.go b/plugin_test.go index 4ad3bc4..6184e04 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -3,9 +3,10 @@ package keyring import ( "testing" - "github.com/launchrctl/launchr" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/launchrctl/launchr" "github.com/launchrctl/launchr/pkg/action" ) @@ -57,17 +58,45 @@ action: - processor: keyring.GetKeyValue ` +const testActionTplFuncValid = ` +action: + title: test keyring +runtime: + type: container + image: alpine + command: + - '{{ keyring.Get "storedsecret" }}' +` + +const testActionTplFuncNotFound = ` +action: + title: test keyring +runtime: + type: container + image: alpine + command: + - '{{ keyring.Get "notexist" }}' +` + +const testActionTplFuncBadArgs = ` +action: + title: test keyring +runtime: + type: container + image: alpine + command: + - '{{ keyring.Get "storedsecret" "storedsecret" }}' +` + func Test_KeyringProcessor(t *testing.T) { // Prepare services. - k := &keyringService{ - store: &dataStoreYaml{file: &plainFile{fname: "teststorage.yaml"}}, - mask: &launchr.SensitiveMask{}, - } + k := NewService(NewFileStore(nil), nil) am := action.NewManager() - addValueProcessors(am, k) + tp := action.NewTemplateProcessors() + addTemplateProcessors(tp, k) // Prepare test data. - expected := "my_secret" + expected := "my_secret" //nolint:goconst // Duplicated constant is ok for tests. err := k.AddItem(KeyValueItem{Key: "storedsecret", Value: expected}) require.NoError(t, err) @@ -88,7 +117,49 @@ func Test_KeyringProcessor(t *testing.T) { tt := tt t.Run(tt.Name, func(t *testing.T) { t.Parallel() - tt.Test(t, am) + tt.Test(t, am, tp) + }) + } +} + +func Test_KeyringTemplate(t *testing.T) { + // Prepare services. + k := NewService(NewFileStore(nil), nil) + tp := action.NewTemplateProcessors() + addTemplateProcessors(tp, k) + svc := launchr.NewServiceManager() + svc.Add(tp) + + // Prepare test data. + expected := "my_secret" + err := k.AddItem(KeyValueItem{Key: "storedsecret", Value: expected}) + require.NoError(t, err) + + type testCase struct { + Name string + Yaml string + Exp []string + Err string + } + tt := []testCase{ + {Name: "valid", Yaml: testActionTplFuncValid, Exp: []string{expected}}, + {Name: "key not found", Yaml: testActionTplFuncNotFound, Err: "\"notexist\" not found in keyring"}, + {Name: "wrong call", Yaml: testActionTplFuncBadArgs, Err: "wrong number of args for Get: want 1 got 2"}, + } + for _, tt := range tt { + tt := tt + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + a := action.NewFromYAML(tt.Name, []byte(tt.Yaml)) + a.SetServices(svc) + err := a.EnsureLoaded() + if tt.Err != "" { + require.ErrorContains(t, err, tt.Err) + return + } + require.NoError(t, err) + rdef := a.RuntimeDef() + assert.Equal(t, tt.Exp, []string(rdef.Container.Command)) }) } } diff --git a/yaml.go b/yaml.go index 94bb458..0b412b1 100644 --- a/yaml.go +++ b/yaml.go @@ -2,6 +2,7 @@ package keyring import ( "errors" + "io" "os" "strings" @@ -44,7 +45,8 @@ func (s *dataStoreYaml) load() error { dec := yaml.NewDecoder(s.file) var strg storage err = dec.Decode(&strg) - if err != nil { + // Yaml library returns io.EOF for an empty file. + if err != nil && err != io.EOF { if strings.Contains(err.Error(), ErrKeyringMalformed.Error()) { // The keyring is malformed, treat it as new. s.file.Lock() From 350be2a4df7f91c31b164713d3b2a0187522c20a Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Mon, 29 Sep 2025 21:46:54 +0200 Subject: [PATCH 2/9] Add tests. --- .github/workflows/ci.yaml | 32 ++++ .github/workflows/commit.yml | 84 -------- Makefile | 2 +- askpass.go | 21 +- example/actions/shell/action.yaml | 12 -- example/actions/subshell/action.yaml | 13 -- file.go | 17 +- go.mod | 36 ++-- go.sum | 35 ++++ keyring.go | 16 +- plugin.go | 54 ++++-- plugin_test.go | 5 +- test/testdata/basic.txtar | 92 +++++++++ test/testdata/passphrase.txtar | 275 +++++++++++++++++++++++++++ test/testdata/sensitive.txtar | 146 ++++++++++++++ test/testdata/tty.txtar | 160 ++++++++++++++++ test/ts_test.go | 29 +++ yaml.go | 8 +- 18 files changed, 852 insertions(+), 185 deletions(-) create mode 100644 .github/workflows/ci.yaml delete mode 100644 .github/workflows/commit.yml delete mode 100644 example/actions/shell/action.yaml delete mode 100644 example/actions/subshell/action.yaml create mode 100644 test/testdata/basic.txtar create mode 100644 test/testdata/passphrase.txtar create mode 100644 test/testdata/sensitive.txtar create mode 100644 test/testdata/tty.txtar create mode 100644 test/ts_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4a60868 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,32 @@ +name: 🧪 Code Coverage & Testing + +on: + push: + branches: + - 'main' + paths-ignore: + - 'README.md' + - 'LICENSE' + - '.gitignore' + - '.goreleaser.yaml' + - 'example/**' + - 'docs/**' + pull_request: + branches: + - 'main' + paths-ignore: + - 'README.md' + - 'LICENSE' + - '.gitignore' + - '.goreleaser.yaml' + - 'example/**' + - 'docs/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: 🛡 Testing Suite + uses: launchrctl/launchr/.github/workflows/test-suite.yaml@main diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml deleted file mode 100644 index 6c5d7e6..0000000 --- a/.github/workflows/commit.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Commit Workflow - -on: - push: - workflow_dispatch: - -jobs: - no-tty-in-ci: - name: Ensure no TTY if user does not need to input anything - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Commands - run: | - set -x - date - pwd - whoami - make deps build - ls -lah bin/launchr - ./bin/launchr keyring:login --url=http://***.git --username="***" --password="***" --keyring-passphrase="***" - echo "***" > my_secret - ./bin/launchr keyring:login --url=http://***.git --username="***" --password="***" --keyring-passphrase-file="my_secret" - - LAUNCHR_KEYRING_PASSPHRASE=mypassphrase ./bin/launchr example:shell - echo "mypassphrase" > my_secret - LAUNCHR_KEYRING_PASSPHRASE_FILE=my_secret ./bin/launchr example:shell - - commands-ok: - name: Ensure main commands do not fail - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Commands - run: | - set -x - date - pwd - whoami - make deps build - ls -lah bin/launchr - ./bin/launchr keyring:set vaultpass "myvaultpass" --keyring-passphrase "mypassphrase" - ls -lah .launchr/keyring.yaml.age - ./bin/launchr keyring:unset vaultpass --keyring-passphrase "mypassphrase" - ls -lah .launchr/keyring.yaml.age - - go-linters: - name: Run linters - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Commands - run: | - set -x - date - pwd - whoami - make lint - - go-tests: - name: Run Go tests - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Commands - run: | - set -x - date - pwd - whoami - make test - diff --git a/Makefile b/Makefile index 44656ab..22c4e8d 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ LOCAL_BIN:=$(CURDIR)/bin # Linter config. GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint -GOLANGCI_TAG:=2.3.0 +GOLANGCI_TAG:=2.5.0 GOTESTFMT_BIN:=$(GOBIN)/gotestfmt diff --git a/askpass.go b/askpass.go index 42a3138..e339284 100644 --- a/askpass.go +++ b/askpass.go @@ -12,7 +12,9 @@ import ( // AskPass defines basic interface to retrieve passphrase. type AskPass interface { + // GetPass retrieves a passphrase for auth. GetPass() (string, error) + // NewPass requests for a new passphrase. NewPass() (string, error) } @@ -80,19 +82,21 @@ func (a AskPassWithTerminal) readPass(prompt string) (string, error) { } // AskPassConst implements AskPass and returns constant. -type AskPassConst func() string +type AskPassConst func() (string, error) // GetPass implements AskPass interface. -func (a AskPassConst) GetPass() (string, error) { return a(), nil } +func (a AskPassConst) GetPass() (string, error) { return a() } // NewPass implements AskPass interface. -func (a AskPassConst) NewPass() (string, error) { return a(), nil } +func (a AskPassConst) NewPass() (string, error) { return a() } +// AskPassFirstAvailable tries a chain of AskPass and returns first available. type AskPassFirstAvailable []AskPass +// GetPass implements AskPass interface. func (a AskPassFirstAvailable) GetPass() (string, error) { - for _, a := range a { - pass, err := a.GetPass() + for _, ask := range a { + pass, err := ask.GetPass() if err != nil { return "", err } @@ -103,9 +107,10 @@ func (a AskPassFirstAvailable) GetPass() (string, error) { return "", nil } +// NewPass implements AskPass interface. func (a AskPassFirstAvailable) NewPass() (string, error) { - for _, a := range a { - pass, err := a.NewPass() + for _, ask := range a { + pass, err := ask.NewPass() if err != nil { return "", err } @@ -113,5 +118,5 @@ func (a AskPassFirstAvailable) NewPass() (string, error) { return pass, nil } } - return "", nil + return "", fmt.Errorf("passphrase is empty") } diff --git a/example/actions/shell/action.yaml b/example/actions/shell/action.yaml deleted file mode 100644 index 918dd1c..0000000 --- a/example/actions/shell/action.yaml +++ /dev/null @@ -1,12 +0,0 @@ -action: - title: test env keyring -runtime: - type: shell - script: | - echo "Main env:" - env | grep "LAUNCHR" - {{ .current_bin }} keyring:purge - {{ .current_bin }} keyring:set storedsecret "my_secret" - echo "Subprocess env:" - {{ .current_bin }} example:subshell - {{ .current_bin }} keyring:purge \ No newline at end of file diff --git a/example/actions/subshell/action.yaml b/example/actions/subshell/action.yaml deleted file mode 100644 index 547334b..0000000 --- a/example/actions/subshell/action.yaml +++ /dev/null @@ -1,13 +0,0 @@ -action: - title: test env keyring - options: - - name: secret - process: - - processor: keyring.GetKeyValue - options: - key: storedsecret -runtime: - type: shell - script: | - env | grep "LAUNCHR" - echo "My secret from keyring: {{ .secret }}" diff --git a/file.go b/file.go index 1d8ce04..8bbbe25 100644 --- a/file.go +++ b/file.go @@ -7,7 +7,6 @@ import ( "strings" "filippo.io/age" - "github.com/launchrctl/launchr" ) @@ -27,19 +26,20 @@ type CredentialsFile interface { type nullFile struct{} -func (_ nullFile) Open(_ int, _ os.FileMode) (err error) { return nil } -func (_ nullFile) Unlock(_ bool) error { return nil } -func (_ nullFile) Lock() {} -func (_ nullFile) Read(_ []byte) (int, error) { return 0, io.EOF } -func (_ nullFile) Write(p []byte) (int, error) { return len(p), nil } -func (_ nullFile) Close() error { return nil } -func (_ nullFile) Remove() error { return nil } +func (nullFile) Open(_ int, _ os.FileMode) (err error) { return nil } +func (nullFile) Unlock(_ bool) error { return nil } +func (nullFile) Lock() {} +func (nullFile) Read(_ []byte) (int, error) { return 0, io.EOF } +func (nullFile) Write(p []byte) (int, error) { return len(p), nil } +func (nullFile) Close() error { return nil } +func (nullFile) Remove() error { return nil } type plainFile struct { fname string file io.ReadWriteCloser } +// NewPlainFile creates a CredentialsFile to open a plain file. func NewPlainFile(fname string) CredentialsFile { return &plainFile{ fname: fname + ".age", @@ -85,6 +85,7 @@ type ageFile struct { w io.WriteCloser } +// NewAgeFile creates a CredentialsFile to open a file encrypted with age. func NewAgeFile(fname string, askPass AskPass) CredentialsFile { return &ageFile{ file: &plainFile{ diff --git a/go.mod b/go.mod index fdf1419..9c1fa11 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,10 @@ module github.com/launchrctl/keyring -go 1.24.0 - -toolchain go1.24.1 +go 1.25.0 require ( filippo.io/age v1.2.1 - github.com/launchrctl/launchr v0.21.2 + github.com/launchrctl/launchr v0.21.3-0.20250928195706-4543b7a29604 github.com/rogpeppe/go-internal v1.14.1 github.com/stretchr/testify v1.11.1 golang.org/x/term v0.35.0 @@ -34,20 +32,20 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.22.0 // indirect - github.com/go-openapi/jsonreference v0.21.1 // indirect - github.com/go-openapi/swag v0.24.1 // indirect - github.com/go-openapi/swag/cmdutils v0.24.0 // indirect - github.com/go-openapi/swag/conv v0.24.0 // indirect - github.com/go-openapi/swag/fileutils v0.24.0 // indirect - github.com/go-openapi/swag/jsonname v0.24.0 // indirect - github.com/go-openapi/swag/jsonutils v0.24.0 // indirect - github.com/go-openapi/swag/loading v0.24.0 // indirect - github.com/go-openapi/swag/mangling v0.24.0 // indirect - github.com/go-openapi/swag/netutils v0.24.0 // indirect - github.com/go-openapi/swag/stringutils v0.24.0 // indirect - github.com/go-openapi/swag/typeutils v0.24.0 // indirect - github.com/go-openapi/swag/yamlutils v0.24.0 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/swag v0.25.1 // indirect + github.com/go-openapi/swag/cmdutils v0.25.1 // indirect + github.com/go-openapi/swag/conv v0.25.1 // indirect + github.com/go-openapi/swag/fileutils v0.25.1 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag/jsonutils v0.25.1 // indirect + github.com/go-openapi/swag/loading v0.25.1 // indirect + github.com/go-openapi/swag/mangling v0.25.1 // indirect + github.com/go-openapi/swag/netutils v0.25.1 // indirect + github.com/go-openapi/swag/stringutils v0.25.1 // indirect + github.com/go-openapi/swag/typeutils v0.25.1 // indirect + github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -60,7 +58,7 @@ require ( github.com/knadh/koanf v1.5.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.17 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index 228a15c..ff81896 100644 --- a/go.sum +++ b/go.sum @@ -122,32 +122,60 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= +github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= +github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= +github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= +github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= +github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= +github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -280,7 +308,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/launchrctl/launchr v0.21.2 h1:D53UHpal9+/Qf+2di5+WS3/9RMWPOJMqeWVtv/gqesI= github.com/launchrctl/launchr v0.21.2/go.mod h1:C7H4FHMSjNi4fUt36rzfyE/2xSpdBdUuq7n7IPAzqEo= +github.com/launchrctl/launchr v0.21.3-0.20250927185244-6f2a4e8fdf34 h1:EiMNfnUIPt1iknHqH+SI+5McfK+MaOkXIe9XJFVeh6E= +github.com/launchrctl/launchr v0.21.3-0.20250927185244-6f2a4e8fdf34/go.mod h1:Z2BRnU5MHVRd1KS06JVcrLf/uXhvgiO0Mkmbcbc0uTo= +github.com/launchrctl/launchr v0.21.3-0.20250928195706-4543b7a29604 h1:2eKPL46IGqbqgkfCEV8PX5qJpb9n566q1ivUZeZuOLc= +github.com/launchrctl/launchr v0.21.3-0.20250928195706-4543b7a29604/go.mod h1:cLETGKQKp6WBg1uPQ2WmrHTjPcWTfseUI0R5NyODxUs= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= @@ -296,6 +329,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky 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.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= diff --git a/keyring.go b/keyring.go index d380a57..f279706 100644 --- a/keyring.go +++ b/keyring.go @@ -7,14 +7,6 @@ import ( "github.com/launchrctl/launchr" ) -type StoreType string - -const ( - StoreTypeInMemoryYaml StoreType = "inmemoryYaml" - StoreTypePlainYamlFile StoreType = "plainYaml" - StoreTypeAgeYamlFile StoreType = "ageYaml" -) - const defaultFileYaml = "keyring.yaml" // Keyring errors. @@ -111,17 +103,14 @@ type DataStore interface { } // Keyring is a [launchr.Service] providing password store functionality. -type Keyring interface { - launchr.Service - DataStore - ResetStorage() -} +type Keyring = *keyringService type keyringService struct { store DataStore mask *launchr.SensitiveMask } +// NewService creates a new Keyring service. func NewService(store DataStore, mask *launchr.SensitiveMask) Keyring { return &keyringService{ store: store, @@ -129,6 +118,7 @@ func NewService(store DataStore, mask *launchr.SensitiveMask) Keyring { } } +// NewFileStore creates a DataStore using a file. func NewFileStore(f CredentialsFile) DataStore { if f == nil { f = nullFile{} diff --git a/plugin.go b/plugin.go index 8ff63ee..41d7e58 100644 --- a/plugin.go +++ b/plugin.go @@ -8,13 +8,13 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" - "golang.org/x/term" - "github.com/launchrctl/launchr" "github.com/launchrctl/launchr/pkg/action" "github.com/launchrctl/launchr/pkg/jsonschema" + "golang.org/x/term" ) const ( @@ -25,8 +25,8 @@ const ( envVarPassphrase = launchr.EnvVar("keyring_passphrase") envVarPassphraseFile = launchr.EnvVar("keyring_passphrase_file") - paramPassphrase = "keyring-passphrase" - paramPassphraseFile = "keyring-passphrase-file" + paramPassphrase = "keyring-passphrase" //nolint:gosec // It's a parameter name. + paramPassphraseFile = "keyring-passphrase-file" //nolint:gosec // It's a parameter name. ) var passphrase = &persistentPassphrase{} @@ -62,16 +62,17 @@ func (p *Plugin) PluginInfo() launchr.PluginInfo { // OnAppInit implements [launchr.Plugin] interface. func (p *Plugin) OnAppInit(app launchr.App) error { - mask := app.SensitiveMask() + var mask *launchr.SensitiveMask var cfg launchr.Config - var tp action.TemplateProcessors + var tp *action.TemplateProcessors app.Services().Get(&cfg) app.Services().Get(&tp) + app.Services().Get(&mask) // Read keyring from a global config directory. // TODO: parse header to know if it's encrypted or not. // TODO: do not encrypt if the passphrase is not provided. - passphrase.mask = app.SensitiveMask() + passphrase.mask = mask store := NewFileStore( NewAgeFile( cfg.Path(defaultFileYaml), @@ -96,7 +97,7 @@ type GetKeyValueProcessorOptions = *action.GenericValueProcessorOptions[struct { }] // addTemplateProcessors adds keyring [action.TemplateProcessors]. -func addTemplateProcessors(tp action.TemplateProcessors, keyring Keyring) { +func addTemplateProcessors(tp *action.TemplateProcessors, keyring Keyring) { tp.AddValueProcessor(procGetKeyValue, action.GenericValueProcessor[GetKeyValueProcessorOptions]{ Types: []jsonschema.Type{jsonschema.String}, Fn: func(v any, opts GetKeyValueProcessorOptions, ctx action.ValueProcessorContext) (any, error) { @@ -104,7 +105,7 @@ func addTemplateProcessors(tp action.TemplateProcessors, keyring Keyring) { }, }) - tp.AddTemplateFunc("keyring", func(ctx action.TemplateFuncContext) any { + tp.AddTemplateFunc("keyring", func(_ action.TemplateFuncContext) any { return func() *keyringTemplateFunc { return &keyringTemplateFunc{k: keyring} } @@ -257,7 +258,7 @@ func (p *Plugin) CobraAddCommands(rootCmd *launchr.Command) error { func (p *Plugin) PersistentPreRun(cmd *launchr.Command, _ []string) error { passphrase.changedPass = cmd.Flags().Changed("keyring-passphrase") passphrase.changedFile = cmd.Flags().Changed("keyring-passphrase-file") - return nil + return passphrase.init() } func buildNotFoundError(item, template string, err error) error { @@ -454,9 +455,9 @@ type persistentPassphrase struct { changedFile bool } -func (p *persistentPassphrase) init() { +func (p *persistentPassphrase) init() error { if p.initialized { - return + return nil } defer func() { @@ -469,34 +470,45 @@ func (p *persistentPassphrase) init() { // Return passphrase if it's already provided. if p.changedPass || p.pass != "" { - return + return nil } // Check env variable for the passphrase. p.pass = envVarPassphrase.Get() if p.pass != "" { - return + return nil } if envPassFile := envVarPassphraseFile.Get(); p.file == "" && envPassFile != "" { - p.file = launchr.MustAbs(envPassFile) + p.file = envPassFile + if !filepath.IsAbs(p.file) { + p.file = launchr.MustAbs(p.file) + } // Override to absolute path. _ = envVarPassphraseFile.Set(p.file) } // Try to read a secret from a file. if p.file != "" { - p.file = launchr.MustAbs(p.file) + if !filepath.IsAbs(p.file) { + p.file = launchr.MustAbs(p.file) + } bytes, err := os.ReadFile(p.file) //nolint:gosec // Filepath checked on previous line. if err != nil { - return + return err } p.pass = strings.TrimSpace(string(bytes)) + if p.pass == "" { + return fmt.Errorf("passphrase file is empty") + } // Set env variable for subprocesses. _ = envVarPassphraseFile.Set(p.file) - return + return nil } + return nil } -func (p *persistentPassphrase) get() string { - p.init() - return p.pass +func (p *persistentPassphrase) get() (string, error) { + if err := p.init(); err != nil { + return "", err + } + return p.pass, nil } diff --git a/plugin_test.go b/plugin_test.go index 6184e04..bf1e62e 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -3,11 +3,10 @@ package keyring import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/launchrctl/launchr" "github.com/launchrctl/launchr/pkg/action" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const testActionYaml = ` diff --git a/test/testdata/basic.txtar b/test/testdata/basic.txtar new file mode 100644 index 0000000..94ed685 --- /dev/null +++ b/test/testdata/basic.txtar @@ -0,0 +1,92 @@ +# ============================================================================= +# Launchr Keyring Basic Functionality Test +# ============================================================================= +# +# This test validates the fundamental keyring operations of Launchr: +# 1. Setting and retrieving key-value pairs in the keyring +# 2. Storing and managing URL-based login credentials +# 3. Keyring file encryption with passphrase protection +# 4. Listing stored keyring entries (keys and URLs) +# 5. Removing individual entries from the keyring +# 6. Complete keyring cleanup and purge operations +# +# Test Focus: +# - Basic CRUD operations on keyring entries +# - Encrypted storage verification (no plaintext secrets) +# - Multi-type storage (key-value pairs and URL credentials) +# - Entry management and cleanup workflows +# ============================================================================= + +# Setup: Configure keyring passphrase via environment variable +# This passphrase will be used to encrypt/decrypt the keyring file +env LAUNCHR_KEYRING_PASSPHRASE='MySecretPassphrase123!' + +# Initial State Verification: Ensure no existing keyring file +# The keyring should not exist before we start our operations +! exists .launchr/keyring.yaml + +# Test 1: Basic Key-Value Storage Operations +# ----------------------------------------------------------------------------- +# Store simple key-value pairs in the keyring +exec launchr keyring:set foo bar +exec launchr keyring:set bar buz + +# Test 2: URL-Based Credential Storage +# ----------------------------------------------------------------------------- +# Store login credentials for specific URLs (useful for web authentication) +exec launchr keyring:login --url example1.com --username foo --password bar +exec launchr keyring:login --url example2.com --username foo --password bar + +# Security Verification: Ensure encrypted storage +# ----------------------------------------------------------------------------- +# Verify that the plaintext keyring file doesn't exist (security requirement) +! exists .launchr/keyring.yaml + +# Verify that the encrypted keyring file was created +exists .launchr/keyring.yaml.age + +# Critical Security Check: Verify no plaintext secrets in encrypted file +# This ensures that sensitive data (URLs, usernames, passwords, keys) are properly encrypted +! grep '(example1\.com|example2\.com|foo|bar|buz)' .launchr/keyring.yaml.age + +# Test 3: Keyring Content Listing and Verification +# ----------------------------------------------------------------------------- +# List all stored entries to verify they were saved correctly +exec launchr keyring:list + +# Verify key-value pairs are listed correctly +stdout 'Key-value pairs:.*\n- foo.*\n- bar' + +# Verify URL credentials are listed correctly +stdout 'URLs:.*\n- example1\.com.*\n- example2\.com' + +# Test 4: Individual Entry Removal Operations +# ----------------------------------------------------------------------------- +# Remove a specific key-value pair from the keyring +exec launchr keyring:unset bar + +# Remove URL credentials for a specific site +exec launchr keyring:logout example2.com + +# Verify encrypted keyring file still exists after partial removal +exists .launchr/keyring.yaml.age + +# Test 5: Verification of Partial Cleanup +# ----------------------------------------------------------------------------- +# Verify that only the remaining entries are listed +exec launchr keyring:list + +# Verify only 'foo' key remains (bar was removed) +stdout 'Key-value pairs:.*\n- foo' + +# Verify only example1.com URL remains (example2.com was removed) +stdout 'URLs:.*\n- example1\.com' + +# Test 6: Complete Keyring Cleanup +# ----------------------------------------------------------------------------- +# Remove all keyring data and the encrypted file +exec launchr keyring:purge + +# Final Verification: Ensure complete cleanup +# Verify that the encrypted keyring file no longer exists +! exists .launchr/keyring.yaml.age \ No newline at end of file diff --git a/test/testdata/passphrase.txtar b/test/testdata/passphrase.txtar new file mode 100644 index 0000000..81d5386 --- /dev/null +++ b/test/testdata/passphrase.txtar @@ -0,0 +1,275 @@ +# ============================================================================= +# Launchr Keyring Passphrase Configuration Test +# ============================================================================= +# +# This test validates the Launchr keyring's passphrase handling functionality: +# 1. Passphrase provided via command-line parameter +# 2. Passphrase provided via environment variable +# 3. Passphrase provided via file parameter +# 4. Passphrase provided via file environment variable +# 5. Priority order of different passphrase sources +# 6. Runtime script access to keyring with various passphrase methods +# +# Test Focus: +# - Multiple passphrase input methods +# - Parameter vs environment variable precedence +# - File-based passphrase handling +# - Cross-platform passphrase file paths +# - Runtime script keyring access +# ============================================================================= + +# Test 1: Passphrase via Command-Line Parameter +# ----------------------------------------------------------------------------- +# Test keyring operations using --keyring-passphrase parameter +exec launchr --keyring-passphrase 'ParameterPassphrase123!' keyring:set param-test param-value + +# Verify keyring file was created with parameter passphrase +exists .launchr/keyring.yaml.age + +# Test retrieval with same parameter passphrase +exec launchr --keyring-passphrase 'ParameterPassphrase123!' keyring:list +stdout 'Key-value pairs:.*\n- param-test' + +# Clean up for next test +exec launchr keyring:purge +! exists .launchr/keyring.yaml.age + +# Test 2: Passphrase via Environment Variable +# ----------------------------------------------------------------------------- +# Test keyring operations using LAUNCHR_KEYRING_PASSPHRASE environment variable +env LAUNCHR_KEYRING_PASSPHRASE='EnvVarPassphrase456!' + +exec launchr keyring:set env-test env-value +exists .launchr/keyring.yaml.age + +exec launchr keyring:list +stdout 'Key-value pairs:.*\n- env-test' + +# Clean up environment and keyring +exec launchr keyring:purge +! exists .launchr/keyring.yaml.age +env LAUNCHR_KEYRING_PASSPHRASE='' + +# Test 3: Passphrase via File Parameter +# ----------------------------------------------------------------------------- +# Create a passphrase file and test using --keyring-passphrase-file parameter + +# Test keyring operations with passphrase file parameter +exec launchr --keyring-passphrase-file passphrase.txt keyring:set file-test file-value +exists .launchr/keyring.yaml.age + +exec launchr --keyring-passphrase-file passphrase.txt keyring:list +stdout 'Key-value pairs:.*\n- file-test' + +# Clean up for next test +exec launchr --keyring-passphrase-file passphrase.txt keyring:purge +! exists .launchr/keyring.yaml.age + +# Test 4: Passphrase via File Environment Variable +# ----------------------------------------------------------------------------- +# Test keyring operations using LAUNCHR_KEYRING_PASSPHRASE_FILE environment variable + +# Set environment variable to point to passphrase file +env LAUNCHR_KEYRING_PASSPHRASE_FILE='env-passphrase.txt' + +exec launchr keyring:set envfile-test envfile-value +exists .launchr/keyring.yaml.age + +exec launchr keyring:list +stdout 'Key-value pairs:.*\n- envfile-test' + +# Clean up environment and keyring +exec launchr keyring:purge +! exists .launchr/keyring.yaml.age +env LAUNCHR_KEYRING_PASSPHRASE_FILE='' + +# Test 5: Precedence Order - Parameter Overrides Environment +# ----------------------------------------------------------------------------- +# Test that command-line parameter takes precedence over environment variable + +env LAUNCHR_KEYRING_PASSPHRASE='EnvPassphrase111!' + +# Create keyring with environment passphrase +exec launchr keyring:set precedence-test precedence-value +exists .launchr/keyring.yaml.age + +# Try to access with different parameter passphrase (should fail) +! exec launchr --keyring-passphrase 'DifferentPassphrase222!' keyring:list + +# Access with correct parameter passphrase (overrides env) +exec launchr --keyring-passphrase 'EnvPassphrase111!' keyring:list +stdout 'Key-value pairs:.*\n- precedence-test' + +# Clean up +exec launchr keyring:purge +! exists .launchr/keyring.yaml.age +env LAUNCHR_KEYRING_PASSPHRASE='' + +# Test 6: Runtime Script Access with Different Passphrase Methods +# ----------------------------------------------------------------------------- +# Test keyring access from runtime scripts using various passphrase methods + +# Setup keyring with environment passphrase for runtime tests +env LAUNCHR_KEYRING_PASSPHRASE='RuntimePassphrase333!' + +exec launchr keyring:set runtime-key runtime-value +exec launchr keyring:login --url example.com --username testuser --password testpass +exists .launchr/keyring.yaml.age + +# Test runtime script with environment passphrase +exec launchr test-keyring:runtime-env +stdout '^Retrieved key: runtime-value$' +stdout '^Retrieved URL credentials: testuser$' + +# Test runtime script with parameter passphrase +exec launchr --keyring-passphrase 'RuntimePassphrase333!' test-keyring:runtime-param +stdout '^Retrieved key with param: runtime-value$' + +# Test runtime script with file passphrase +exec launchr --keyring-passphrase-file runtime-passphrase.txt test-keyring:runtime-file +stdout '^Retrieved key with file: runtime-value$' + +# Clean up +exec launchr keyring:purge +! exists .launchr/keyring.yaml.age +env LAUNCHR_KEYRING_PASSPHRASE='' + +# Test 7: Cross-Platform File Path Handling +# ----------------------------------------------------------------------------- +# Test passphrase file handling with different path formats + +# Test with relative path +exec launchr --keyring-passphrase-file config/secret.txt keyring:set cross-platform-test cross-value +exists .launchr/keyring.yaml.age + +exec launchr --keyring-passphrase-file config/secret.txt keyring:list +stdout 'Key-value pairs:.*\n- cross-platform-test' + +# Test with absolute path via environment variable +env LAUNCHR_KEYRING_PASSPHRASE_FILE=$WORK/config/secret.txt +exec launchr keyring:list +stdout 'Key-value pairs:.*\n- cross-platform-test' + +# Clean up +exec launchr keyring:purge +! exists .launchr/keyring.yaml.age +env LAUNCHR_KEYRING_PASSPHRASE_FILE='' + +# Test 8: Error Handling for Invalid Passphrase Sources +# ----------------------------------------------------------------------------- +# Test error handling when passphrase sources are invalid + +# Test with non-existent passphrase file +! exec launchr --keyring-passphrase-file non-existent.txt keyring:set error-test error-value + +# Test with empty passphrase file +! exec launchr --keyring-passphrase-file empty-passphrase.txt keyring:set empty-test empty-value + +! exists .launchr/keyring.yaml.age + +# ============================================================================= +# Test Data Files - Runtime Script Actions +# ============================================================================= + +# Runtime Action with Environment Passphrase +-- test-keyring/actions/runtime-env/action.yaml -- +action: + title: keyring runtime test - environment passphrase + +runtime: + type: shell + script: | + # Test keyring access with environment passphrase + value=$($$CBIN keyring:list | grep runtime-key || echo "not found") + if echo "$$value" | grep -q "runtime-key"; then + echo "Retrieved key: runtime-value" + else + echo "Failed to retrieve key" + exit 1 + fi + + # Test URL credentials access + creds=$($$CBIN keyring:list | grep example.com || echo "not found") + if echo "$$creds" | grep -q "example.com"; then + echo "Retrieved URL credentials: testuser" + else + echo "Failed to retrieve credentials" + exit 1 + fi + +# Runtime Action with Parameter Passphrase +-- test-keyring/actions/runtime-param/action.yaml -- +action: + title: keyring runtime test - parameter passphrase + +runtime: + type: shell + script: | + # Test keyring access with parameter passphrase + value=$($$CBIN keyring:list | grep runtime-key || echo "not found") + if echo "$$value" | grep -q "runtime-key"; then + echo "Retrieved key with param: runtime-value" + else + echo "Failed to retrieve key with param" + exit 1 + fi + +# Runtime Action with File Passphrase +-- test-keyring/actions/runtime-file/action.yaml -- +action: + title: keyring runtime test - file passphrase + +runtime: + type: shell + script: | + # Test keyring access with file passphrase + value=$($$CBIN keyring:list | grep runtime-key || echo "not found") + if echo "$$value" | grep -q "runtime-key"; then + echo "Retrieved key with file: runtime-value" + else + echo "Failed to retrieve key with file" + exit 1 + fi + +-- empty-passphrase.txt -- + +-- passphrase.txt -- +FilePassphrase789! + +-- env-passphrase.txt -- +EnvFilePassphrase000! + +-- runtime-passphrase.txt -- +RuntimePassphrase333! + +-- config/secret.txt -- +CrossPlatformPass444! + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Passphrase Source Priority (highest to lowest): +# 1. --keyring-passphrase parameter +# 2. LAUNCHR_KEYRING_PASSPHRASE environment variable +# 3. --keyring-passphrase-file parameter +# 4. LAUNCHR_KEYRING_PASSPHRASE_FILE environment variable +# +# File Handling: +# 1. Passphrase files are read and trimmed of whitespace +# 2. Relative and absolute paths are supported +# 3. Non-existent files cause errors +# 4. Empty files result in empty passphrase (unencrypted keyring) +# +# Runtime Integration: +# 1. Runtime scripts can access keyring using any passphrase method +# 2. Environment variables are inherited by runtime scripts +# 3. Parameters must be explicitly passed to nested launchr calls +# 4. File passphrases work with relative paths from runtime context +# +# Security Considerations: +# 1. Passphrases are masked in logs and output +# 2. File passphrases are more secure than command-line parameters +# 3. Environment variables are inherited by subprocesses +# 4. Empty passphrases result in unencrypted storage +# ============================================================================= diff --git a/test/testdata/sensitive.txtar b/test/testdata/sensitive.txtar new file mode 100644 index 0000000..175daf5 --- /dev/null +++ b/test/testdata/sensitive.txtar @@ -0,0 +1,146 @@ +# ============================================================================= +# Launchr Keyring Sensitive Data Protection Test +# ============================================================================= +# +# This test validates the Launchr keyring's sensitive data protection mechanisms: +# 1. Environment variable masking in command output and logs +# 2. Keyring content masking when displayed in runtime scripts +# 3. Passphrase protection across different input methods (env var vs file) +# 4. Cross-process sensitive data handling (parent to subprocess) +# 5. Template rendering security for keyring values +# 6. Multi-level script execution with consistent data protection +# +# Test Focus: +# - Sensitive data never appears in plaintext in output +# - Consistent masking across environment variables and keyring values +# - Protection maintained through subprocess execution chains +# - Template system respects sensitive data masking rules +# - Different passphrase input methods maintain same security level +# +# Security Requirements: +# - Passphrases must be masked as "****" in all output +# - Keyring values must be masked as "****" when displayed +# - Environment variable names can be shown, but not values +# - Subprocess inheritance maintains protection levels +# ============================================================================= + +# Test 1: Environment Variable Passphrase with Sensitive Data Protection +# ----------------------------------------------------------------------------- +# Test keyring operations using environment variable passphrase +# Validate that sensitive data is properly masked in all output contexts + +# Configure keyring passphrase via environment variable +env LAUNCHR_KEYRING_PASSPHRASE=MySecretPassphrase123! + +# Execute shell action that displays environment and uses keyring +exec launchr example:shell + +# Critical Security Checks: Verify no plaintext sensitive data in output +# Passphrase value should never appear in plaintext +! stdout $LAUNCHR_KEYRING_PASSPHRASE +# Keyring stored values should never appear in plaintext +! stdout my_secret + +# Verify Proper Masking: Sensitive data should be masked as "****" +# Environment variable should be shown as masked (appears twice in output) +stdout -count=2 'LAUNCHR_KEYRING_PASSPHRASE=\*\*\*\*' +# Keyring template output should be masked +stdout 'My secret from keyring: \*\*\*\*' + +# Test Cleanup: Remove keyring data and clear environment +exec launchr keyring:purge +env LAUNCHR_KEYRING_PASSPHRASE='' + +# Test 2: File-Based Passphrase with Sensitive Data Protection +# ----------------------------------------------------------------------------- +# Test keyring operations using file-based passphrase configuration +# Verify consistent protection across different passphrase input methods + +# Configure keyring passphrase via file reference +env LAUNCHR_KEYRING_PASSPHRASE_FILE=passphrase.txt + +# Execute same shell action with file-based passphrase +exec launchr example:shell + +# Security Verification: No plaintext passphrase content from file +! stdout 'MySecretPassphrase123!' +# No plaintext keyring values +! stdout my_secret + +# Verify File Reference Display: File path can be shown, but not content +# Environment variable should show file path (appears twice) +stdout -count=2 'LAUNCHR_KEYRING_PASSPHRASE_FILE=.*passphrase\.txt' +# Keyring template output should still be masked +stdout 'My secret from keyring: \*\*\*\*' + +# ============================================================================= +# Test Data Files - Passphrase and Runtime Actions +# ============================================================================= + +# Passphrase File: Contains the actual passphrase for file-based authentication +# This file simulates external passphrase storage for enhanced security +-- passphrase.txt -- +MySecretPassphrase123! + +# Primary Shell Action: Demonstrates environment inspection and keyring usage +# This action tests both environment variable display and keyring operations +-- example/actions/shell/action.yaml -- +action: + title: test env keyring + +runtime: + type: shell + script: | + # Display environment variables (should show masked sensitive values) + env | grep "LAUNCHR" + + # Store a secret value in the keyring for testing + $$CBIN keyring:set storedsecret "my_secret" + + # Call subprocess to test cross-process sensitive data handling + echo "Subprocess env:" + $$CBIN example:subshell + +# Subprocess Shell Action: Tests inherited environment and keyring template access +# This action validates that sensitive data protection is maintained across process boundaries +-- example/actions/subshell/action.yaml -- +action: + title: test env keyring + +runtime: + type: shell + script: | + # Display inherited environment (should maintain masking from parent) + env | grep "LAUNCHR" + + # Access keyring value via template (should be masked in output) + echo "My secret from keyring: {{ keyring.Get "storedsecret" }}" + +# ============================================================================= +# Expected Behavior Summary +# ============================================================================= +# +# Environment Variable Protection: +# 1. LAUNCHR_KEYRING_PASSPHRASE values are masked as "****" +# 2. LAUNCHR_KEYRING_PASSPHRASE_FILE shows file path but not content +# 3. Environment variable names are visible, values are protected +# 4. Protection is maintained across subprocess calls +# +# Keyring Value Protection: +# 1. Template rendering masks sensitive values as "****" +# 2. Direct keyring values never appear in plaintext in logs +# 3. Storage operations don't leak values to output +# 4. Cross-process template access maintains masking +# +# Multi-Process Security: +# 1. Parent process masking rules inherited by subprocesses +# 2. Environment variable protection consistent across call chain +# 3. Keyring access security maintained regardless of call depth +# 4. Template system respects masking in all execution contexts +# +# Passphrase Input Method Independence: +# 1. Same protection level regardless of passphrase source +# 2. Environment variable vs file-based input both secure +# 3. Consistent masking behavior across input methods +# 4. File content protection (path shown, content masked) +# ============================================================================= \ No newline at end of file diff --git a/test/testdata/tty.txtar b/test/testdata/tty.txtar new file mode 100644 index 0000000..39d601c --- /dev/null +++ b/test/testdata/tty.txtar @@ -0,0 +1,160 @@ +# ============================================================================= +# Launchr Keyring TTY Interactive Input Test +# ============================================================================= +# +# This test validates the Launchr keyring's interactive terminal (TTY) functionality: +# 1. Interactive passphrase input for new keyring creation +# 2. Interactive passphrase input for existing keyring access +# 3. Passphrase confirmation prompts for new keyrings +# 4. Security verification (no plaintext secrets in output) +# 5. Environment variable handling in TTY mode +# 6. Multiple keyring operations with TTY input +# +# Test Focus: +# - Interactive terminal input simulation +# - Secure passphrase handling (no echo/display) +# - New vs existing keyring workflow differences +# - Cross-platform TTY compatibility +# - Output security validation +# +# Security Considerations: +# - Passphrases should never appear in stdout/stderr +# - Environment variable values should not leak to output +# - TTY input should be handled securely across operations +# ============================================================================= + +# TODO: We have functionality to fill missing keys. Current implementation doesn't work well with TestScript. +# TODO: It's unclear if we can really test that the password is not visible when typing. + +# Platform Compatibility Check +# ----------------------------------------------------------------------------- +# TTY functionality testing requires Unix-like terminal support +# Windows platforms have different TTY handling that's not compatible with this test +[windows] skip 'Testing TTY is not supported on Windows' + +# Test Setup: Environment Variables for Secure Testing +# ----------------------------------------------------------------------------- +# Configure test credentials via environment variables to avoid hardcoding +# These simulate real-world usage where sensitive data comes from environment +env TEST_KEYRING_PASSPHRASE=MyPassphrase123! +env TEST_USERNAME=MyLoginUser +env TEST_PASSWORD=MyLoginPass + +# Test 1: New Keyring Creation with TTY Input +# ----------------------------------------------------------------------------- +# Simulate interactive input for creating a new keyring +# This test validates the initial keyring setup workflow with passphrase confirmation + +# Configure TTY input stream with passphrase + confirmation +ttyin -stdin tty.new-passphrase + +# Execute keyring login command that should trigger new keyring creation +exec launchr keyring:login --url=http://example.com/project1 --username="$TEST_USERNAME" --password="$TEST_PASSWORD" + +# Verify Interactive Prompts: New keyring should prompt for passphrase creation +stdout '^Enter passphrase for a new keyring:.*' +stdout '^Confirm passphrase:.*' + +# Critical Security Verification: Ensure no plaintext secrets in output +# Environment variable values should never appear in command output +! stdout '$TEST_KEYRING_PASSPHRASE' +! stdout '$TEST_USERNAME' +! stdout '$TEST_PASSWORD' +! stderr . + +# Test 2: Existing Keyring Access with TTY Input +# ----------------------------------------------------------------------------- +# Test accessing an existing keyring with interactive passphrase input +# This validates the unlock workflow for previously created keyrings + +# Configure TTY input for existing keyring unlock +ttyin -stdin tty.existing-passphrase + +# Add another URL credential to the existing keyring +exec launchr keyring:login --url=http://example.com/project2 --username="$TEST_USERNAME" --password="$TEST_PASSWORD" + +# Verify Existing Keyring Prompt: Should only ask for unlock passphrase +stdout '^Enter passphrase to unlock the keyring:.*' + +# Security Verification: No sensitive data should appear in output +! stdout '$TEST_KEYRING_PASSPHRASE' +! stdout '$TEST_USERNAME' +! stdout '$TEST_PASSWORD' +! stderr . + +# Test 3: Key-Value Operations with TTY Input +# ----------------------------------------------------------------------------- +# Test setting key-value pairs with interactive passphrase input +# This validates TTY functionality for non-URL keyring operations + +ttyin -stdin tty.existing-passphrase +exec launchr keyring:set foo bar + +# Verify unlock prompt for key-value operations +stdout '^Enter passphrase to unlock the keyring:.*' + +# Security Check: Key-value data should not appear in prompts +! stdout '$TEST_KEYRING_PASSPHRASE' +! stdout 'foo' +! stdout 'bar' +! stderr . + +# Test 4: Keyring Listing with TTY Input and Content Verification +# ----------------------------------------------------------------------------- +# Test listing keyring contents with interactive unlock +# This validates both TTY input and proper data storage/retrieval + +ttyin -stdin tty.existing-passphrase +exec launchr keyring:list + +# Verify unlock prompt for list operations +stdout '^Enter passphrase to unlock the keyring:.*' + +# Verify Content Display: After unlock, should show stored data +stdout 'Key-value pairs:.*\n- foo' +stdout 'URLs:.*\n- http://example\.com/project1.*\n- http://example\.com/project2' + +# Test 5: Fresh Keyring Creation After Purge +# ----------------------------------------------------------------------------- +# Test creating a new keyring after purging existing data +# This validates the complete reset and recreation workflow + +# Clean up existing keyring data +exec launchr keyring:purge + +# Create new keyring with TTY input (should prompt for new passphrase again) +ttyin -stdin tty.new-passphrase +exec launchr keyring:set foo bar + +# Verify New Keyring Prompts: Should ask for new passphrase + confirmation +stdout '^Enter passphrase for a new keyring:.*' +stdout '^Confirm passphrase:.*' + +# Final Security Verification: No sensitive data in output +! stdout '$TEST_KEYRING_PASSPHRASE' +! stdout 'foo' +! stdout 'bar' +! stderr . + +# ============================================================================= +# TTY Input Simulation Files +# ============================================================================= +# +# These files simulate user keyboard input for interactive prompts: +# +# tty.new-passphrase: +# - First line: Initial passphrase entry +# - Second line: Passphrase confirmation +# Both must match for successful keyring creation +# +# tty.existing-passphrase: +# - Single line: Passphrase for unlocking existing keyring +# Must match the passphrase used during creation +# ============================================================================= + +-- ./tty.new-passphrase -- +MyPassphrase123! +MyPassphrase123! + +-- ./tty.existing-passphrase -- +MyPassphrase123! \ No newline at end of file diff --git a/test/ts_test.go b/test/ts_test.go new file mode 100644 index 0000000..eaf1b41 --- /dev/null +++ b/test/ts_test.go @@ -0,0 +1,29 @@ +package test + +import ( + "testing" + "time" + + "github.com/rogpeppe/go-internal/testscript" + + "github.com/launchrctl/launchr" + + _ "github.com/launchrctl/keyring" +) + +func TestMain(m *testing.M) { + testscript.Main(m, map[string]func(){ + "launchr": launchr.RunAndExit, + }) +} + +func TestKeyring(t *testing.T) { + t.Parallel() + testscript.Run(t, testscript.Params{ + Dir: "testdata", + Deadline: time.Now().Add(30 * time.Second), + + RequireExplicitExec: true, + RequireUniqueNames: true, + }) +} diff --git a/yaml.go b/yaml.go index 0b412b1..786f7f5 100644 --- a/yaml.go +++ b/yaml.go @@ -221,14 +221,16 @@ func (s *dataStoreYaml) Exists() bool { // Save implements DataStore interface. func (s *dataStoreYaml) Save() error { + // Try to unlock first. + // If the file has not been created yet, we won't create an empty file on a wrong passphrase. + if err := s.file.Unlock(true); err != nil { + return err + } err := s.file.Open(os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0600) if err != nil { return err } defer s.file.Close() - if err = s.file.Unlock(true); err != nil { - return err - } enc := yaml.NewEncoder(s.file) return enc.Encode(s.data) } From 6576810189ec36e59c9bdc734b0c5cbb07603741 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Mon, 29 Sep 2025 22:00:09 +0200 Subject: [PATCH 3/9] Rename template function --- README.md | 2 +- plugin.go | 7 ++----- plugin_test.go | 8 ++++---- test/testdata/sensitive.txtar | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 06ac419..0f52179 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ import ( func GetPassword(app launchr.App, url string) (keyring.CredentialsItem, error) { // Get the service by type from the app. var k keyring.Keyring - app.GetService(k) + app.Services().Get(k) // Get by url. Error if the keyring could not be unlocked. // Error keyring.ErrNotFound is returned if an item was not found. creds, err := k.GetForURL(url) diff --git a/plugin.go b/plugin.go index 41d7e58..aaf805d 100644 --- a/plugin.go +++ b/plugin.go @@ -105,11 +105,8 @@ func addTemplateProcessors(tp *action.TemplateProcessors, keyring Keyring) { }, }) - tp.AddTemplateFunc("keyring", func(_ action.TemplateFuncContext) any { - return func() *keyringTemplateFunc { - return &keyringTemplateFunc{k: keyring} - } - }) + ktpl := &keyringTemplateFunc{k: keyring} + tp.AddTemplateFunc("keyring", ktpl.Get) } func processGetByKey(value any, opts GetKeyValueProcessorOptions, ctx action.ValueProcessorContext, k Keyring) (any, error) { diff --git a/plugin_test.go b/plugin_test.go index bf1e62e..afbca08 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -64,7 +64,7 @@ runtime: type: container image: alpine command: - - '{{ keyring.Get "storedsecret" }}' + - '{{ keyring "storedsecret" }}' ` const testActionTplFuncNotFound = ` @@ -74,7 +74,7 @@ runtime: type: container image: alpine command: - - '{{ keyring.Get "notexist" }}' + - '{{ keyring "notexist" }}' ` const testActionTplFuncBadArgs = ` @@ -84,7 +84,7 @@ runtime: type: container image: alpine command: - - '{{ keyring.Get "storedsecret" "storedsecret" }}' + - '{{ keyring "storedsecret" "storedsecret" }}' ` func Test_KeyringProcessor(t *testing.T) { @@ -143,7 +143,7 @@ func Test_KeyringTemplate(t *testing.T) { tt := []testCase{ {Name: "valid", Yaml: testActionTplFuncValid, Exp: []string{expected}}, {Name: "key not found", Yaml: testActionTplFuncNotFound, Err: "\"notexist\" not found in keyring"}, - {Name: "wrong call", Yaml: testActionTplFuncBadArgs, Err: "wrong number of args for Get: want 1 got 2"}, + {Name: "wrong call", Yaml: testActionTplFuncBadArgs, Err: "wrong number of args for keyring: want 1 got 2"}, } for _, tt := range tt { tt := tt diff --git a/test/testdata/sensitive.txtar b/test/testdata/sensitive.txtar index 175daf5..f4d6c88 100644 --- a/test/testdata/sensitive.txtar +++ b/test/testdata/sensitive.txtar @@ -114,7 +114,7 @@ runtime: env | grep "LAUNCHR" # Access keyring value via template (should be masked in output) - echo "My secret from keyring: {{ keyring.Get "storedsecret" }}" + echo "My secret from keyring: {{ keyring "storedsecret" }}" # ============================================================================= # Expected Behavior Summary From cef49a18ea037af92a379b481883205044eb63ad Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Tue, 30 Sep 2025 00:43:23 +0200 Subject: [PATCH 4/9] Implement new ServiceCreate for a keyring. --- keyring.go | 22 ++++++++++++++++++++++ plugin.go | 25 ++----------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/keyring.go b/keyring.go index f279706..7200010 100644 --- a/keyring.go +++ b/keyring.go @@ -131,6 +131,28 @@ func (k *keyringService) ServiceInfo() launchr.ServiceInfo { return launchr.ServiceInfo{} } +func (k *keyringService) ServiceCreate(svc *launchr.ServiceManager) launchr.Service { + var cfg launchr.Config + var mask *launchr.SensitiveMask + svc.Get(&cfg) + svc.Get(&mask) + + // Read keyring from a global config directory. + // TODO: parse header to know if it's encrypted or not. + // TODO: do not encrypt if the passphrase is not provided. + store := NewFileStore( + NewAgeFile( + cfg.Path(defaultFileYaml), + AskPassFirstAvailable{ + AskPassConst(passphrase.get), + AskPassWithTerminal{}, + }, + ), + ) + + return NewService(store, mask) +} + // ResetStorage cleans store for subsequent reload. func (k *keyringService) ResetStorage() { k.store = nil diff --git a/plugin.go b/plugin.go index aaf805d..c490a21 100644 --- a/plugin.go +++ b/plugin.go @@ -62,32 +62,11 @@ func (p *Plugin) PluginInfo() launchr.PluginInfo { // OnAppInit implements [launchr.Plugin] interface. func (p *Plugin) OnAppInit(app launchr.App) error { - var mask *launchr.SensitiveMask - var cfg launchr.Config var tp *action.TemplateProcessors - app.Services().Get(&cfg) app.Services().Get(&tp) - app.Services().Get(&mask) - - // Read keyring from a global config directory. - // TODO: parse header to know if it's encrypted or not. - // TODO: do not encrypt if the passphrase is not provided. - passphrase.mask = mask - store := NewFileStore( - NewAgeFile( - cfg.Path(defaultFileYaml), - AskPassFirstAvailable{ - AskPassConst(passphrase.get), - AskPassWithTerminal{}, - }, - ), - ) - - p.k = NewService(store, mask) - app.Services().Add(p.k) - + app.Services().Get(&passphrase.mask) + app.Services().Get(&p.k) addTemplateProcessors(tp, p.k) - return nil } From 376fc16937898945d2a1860604d8c0b3d9fe7142 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Tue, 30 Sep 2025 23:42:37 +0200 Subject: [PATCH 5/9] Fix after review. --- file.go | 35 +++++++---- keyring.go | 126 ++++++-------------------------------- plugin.go | 4 +- plugin_test.go | 62 +++++++++++++++++++ test/testdata/basic.txtar | 22 ++++++- yaml.go | 15 +++-- 6 files changed, 133 insertions(+), 131 deletions(-) diff --git a/file.go b/file.go index 8bbbe25..256d9b8 100644 --- a/file.go +++ b/file.go @@ -1,7 +1,9 @@ package keyring import ( + "errors" "io" + "io/fs" "os" "path/filepath" "strings" @@ -15,17 +17,22 @@ type CredentialsFile interface { io.ReadWriteCloser // Open opens a file in FS with flag open options and perm for file permissions if the file is new. // See os.OpenFile for more info about flag and perm arguments. - Open(flag int, perm os.FileMode) error + Open(flag int, perm fs.FileMode) error // Unlock decrypts a file if supported. Unlock(askNew bool) error // Lock makes it to request Unlock again. Lock() // Remove deletes a file from FS. Remove() error + // Stat returns a [FileInfo] describing the named file. + // If there is an error, it will be of type [*PathError]. + // See os.Stat(). + Stat() (fs.FileInfo, error) } type nullFile struct{} +func (nullFile) Stat() (fs.FileInfo, error) { return nil, fs.ErrNotExist } func (nullFile) Open(_ int, _ os.FileMode) (err error) { return nil } func (nullFile) Unlock(_ bool) error { return nil } func (nullFile) Lock() {} @@ -42,11 +49,11 @@ type plainFile struct { // NewPlainFile creates a CredentialsFile to open a plain file. func NewPlainFile(fname string) CredentialsFile { return &plainFile{ - fname: fname + ".age", + fname: fname, } } -func (f *plainFile) Open(flag int, perm os.FileMode) (err error) { +func (f *plainFile) Open(flag int, perm fs.FileMode) (err error) { isCreate := flag&os.O_CREATE == os.O_CREATE if isCreate { err = launchr.EnsurePath(filepath.Dir(f.fname)) @@ -63,6 +70,7 @@ func (f *plainFile) Open(flag int, perm os.FileMode) (err error) { return nil } +func (f *plainFile) Stat() (fs.FileInfo, error) { return os.Stat(f.fname) } func (f *plainFile) Unlock(bool) (err error) { return nil } func (f *plainFile) Lock() {} func (f *plainFile) Read(p []byte) (n int, err error) { return f.file.Read(p) } @@ -77,7 +85,7 @@ func (f *plainFile) Remove() (err error) { } type ageFile struct { - file *plainFile + *plainFile askPass AskPass passphrase string // @todo make sure it's compatible with ACL in the future @@ -88,16 +96,12 @@ type ageFile struct { // NewAgeFile creates a CredentialsFile to open a file encrypted with age. func NewAgeFile(fname string, askPass AskPass) CredentialsFile { return &ageFile{ - file: &plainFile{ - fname: fname + ".age", - }, - askPass: askPass, + plainFile: NewPlainFile(fname).(*plainFile), + askPass: askPass, } } -func (f *ageFile) Open(flag int, perm os.FileMode) (err error) { return f.file.Open(flag, perm) } -func (f *ageFile) Remove() error { return f.file.Remove() } -func (f *ageFile) Lock() { f.passphrase = "" } +func (f *ageFile) Lock() { f.passphrase = "" } func (f *ageFile) Unlock(askNew bool) (err error) { if f.passphrase != "" { @@ -129,6 +133,8 @@ func (f *ageFile) Read(p []byte) (n int, err error) { // The file is malformed, not age encrypted and can't be read. if strings.Contains(err.Error(), "parsing age header:") { return 0, ErrKeyringMalformed + } else if strings.Contains(err.Error(), "no identity matched any of the recipients") { + return 0, ErrIncorrectPass } return 0, err } @@ -152,8 +158,11 @@ func (f *ageFile) Write(p []byte) (n int, err error) { } func (f *ageFile) Close() error { + var err error if f.w != nil { - _ = f.w.Close() + err = f.w.Close() } - return f.file.Close() + f.w = nil + f.r = nil + return errors.Join(err, f.file.Close()) } diff --git a/keyring.go b/keyring.go index 7200010..cdc40c7 100644 --- a/keyring.go +++ b/keyring.go @@ -11,10 +11,11 @@ const defaultFileYaml = "keyring.yaml" // Keyring errors. var ( - ErrNotFound = errors.New("item not found") // ErrNotFound if an item was not found - ErrEmptyFields = errors.New("item can't be empty") // ErrEmptyFields if fields are empty - ErrEmptyPass = errors.New("passphrase can't be empty") // ErrEmptyPass if a passphrase is empty - ErrKeyringMalformed = errors.New("the keyring is malformed") // ErrKeyringMalformed when keyring can't be read. + ErrNotFound = errors.New("item not found") // ErrNotFound if an item was not found + ErrEmptyFields = errors.New("item can't be empty") // ErrEmptyFields if fields are empty + ErrEmptyPass = errors.New("passphrase can't be empty") // ErrEmptyPass if a passphrase is empty + ErrKeyringMalformed = errors.New("the keyring is malformed") // ErrKeyringMalformed when keyring can't be read. + ErrIncorrectPass = errors.New("the given passphrase is incorrect") // ErrIncorrectPass if a passphrase is incorrect ) // SecretItem is an interface that represents an item saved in a storage. @@ -102,19 +103,22 @@ type DataStore interface { Destroy() error } +// dataStore is a type alias to embed it as a private property. +type dataStore = DataStore + // Keyring is a [launchr.Service] providing password store functionality. type Keyring = *keyringService type keyringService struct { - store DataStore - mask *launchr.SensitiveMask + dataStore + mask *launchr.SensitiveMask } // NewService creates a new Keyring service. func NewService(store DataStore, mask *launchr.SensitiveMask) Keyring { return &keyringService{ - store: store, - mask: mask, + dataStore: store, + mask: mask, } } @@ -142,7 +146,7 @@ func (k *keyringService) ServiceCreate(svc *launchr.ServiceManager) launchr.Serv // TODO: do not encrypt if the passphrase is not provided. store := NewFileStore( NewAgeFile( - cfg.Path(defaultFileYaml), + cfg.Path(defaultFileYaml+".age"), AskPassFirstAvailable{ AskPassConst(passphrase.get), AskPassWithTerminal{}, @@ -153,42 +157,9 @@ func (k *keyringService) ServiceCreate(svc *launchr.ServiceManager) launchr.Serv return NewService(store, mask) } -// ResetStorage cleans store for subsequent reload. -func (k *keyringService) ResetStorage() { - k.store = nil -} - -func (k *keyringService) defaultStore() (DataStore, error) { - return k.store, nil -} - -// GetUrls implements DataStore interface. Uses service default store. -func (k *keyringService) GetUrls() ([]string, error) { - s, err := k.defaultStore() - if err != nil { - return []string{}, err - } - - return s.GetUrls() -} - -// GetKeys implements DataStore interface. Uses service default store. -func (k *keyringService) GetKeys() ([]string, error) { - s, err := k.defaultStore() - if err != nil { - return []string{}, err - } - - return s.GetKeys() -} - // GetForURL implements DataStore interface. Uses service default store. func (k *keyringService) GetForURL(url string) (CredentialsItem, error) { - s, err := k.defaultStore() - if err != nil { - return CredentialsItem{}, err - } - item, err := s.GetForURL(url) + item, err := k.dataStore.GetForURL(url) if err == nil { k.maskItem(item) } @@ -197,11 +168,7 @@ func (k *keyringService) GetForURL(url string) (CredentialsItem, error) { // GetForKey implements DataStore interface. Uses service default store. func (k *keyringService) GetForKey(key string) (KeyValueItem, error) { - s, err := k.defaultStore() - if err != nil { - return KeyValueItem{}, err - } - item, err := s.GetForKey(key) + item, err := k.dataStore.GetForKey(key) if err == nil { k.maskItem(item) } @@ -210,18 +177,15 @@ func (k *keyringService) GetForKey(key string) (KeyValueItem, error) { // AddItem implements DataStore interface. Uses service default store. func (k *keyringService) AddItem(item SecretItem) error { - s, err := k.defaultStore() - if err != nil { - return err - } - k.maskItem(item) - return s.AddItem(item) + return k.dataStore.AddItem(item) } // MaskItem masks the item values func (k *keyringService) maskItem(item SecretItem) { if k.mask == nil { + // Mask may be nil in unit tests for simplicity. + // Mask is checked in e2e tests. return } switch dataItem := item.(type) { @@ -234,57 +198,3 @@ func (k *keyringService) maskItem(item SecretItem) { default: } } - -// RemoveByURL implements DataStore interface. Uses service default store. -func (k *keyringService) RemoveByURL(url string) error { - s, err := k.defaultStore() - if err != nil { - return err - } - return s.RemoveByURL(url) -} - -// RemoveByKey implements DataStore interface. Uses service default store. -func (k *keyringService) RemoveByKey(key string) error { - s, err := k.defaultStore() - if err != nil { - return err - } - return s.RemoveByKey(key) -} - -// CleanStorage implements DataStore interface. Uses service default store. -func (k *keyringService) CleanStorage(item SecretItem) error { - s, err := k.defaultStore() - if err != nil { - return err - } - return s.CleanStorage(item) -} - -// Exists implements DataStore, checks if keyring exists in persistent storage. -func (k *keyringService) Exists() bool { - s, err := k.defaultStore() - if err != nil { - return false - } - return s.Exists() -} - -// Save implements DataStore interface. Uses service default store. -func (k *keyringService) Save() error { - s, err := k.defaultStore() - if err != nil { - return err - } - return s.Save() -} - -// Destroy implements DataStore interface. Uses service default store. -func (k *keyringService) Destroy() error { - s, err := k.defaultStore() - if err != nil { - return err - } - return s.Destroy() -} diff --git a/plugin.go b/plugin.go index c490a21..bee074a 100644 --- a/plugin.go +++ b/plugin.go @@ -114,8 +114,6 @@ func processGetByKey(value any, opts GetKeyValueProcessorOptions, ctx action.Val return value, err } - // Ensure keyring storage will be accessible after save. - defer k.ResetStorage() err = k.Save() if err != nil { return value, err @@ -438,7 +436,7 @@ func (p *persistentPassphrase) init() error { defer func() { // If the passphrase is set with user input or env variable, hide it. - if p.file == "" && p.pass != "" { + if p.mask != nil && p.file == "" && p.pass != "" { p.mask.AddString(p.pass) } p.initialized = true diff --git a/plugin_test.go b/plugin_test.go index afbca08..7071fc2 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -1,6 +1,7 @@ package keyring import ( + "path/filepath" "testing" "github.com/launchrctl/launchr" @@ -162,3 +163,64 @@ func Test_KeyringTemplate(t *testing.T) { }) } } + +func Test_KeyringSave(t *testing.T) { + type testCase struct { + name string + dataStore func(string) DataStore + } + tt := []testCase{ + {"plain file", func(dir string) DataStore { + return NewFileStore( + NewPlainFile(filepath.Join(dir, defaultFileYaml)), + ) + }}, + {"age file", func(dir string) DataStore { + testPass := persistentPassphrase{pass: "pass"} + return NewFileStore( + NewAgeFile( + filepath.Join(dir, defaultFileYaml+".age"), + AskPassConst(testPass.get), + ), + ) + }}, + } + for _, tt := range tt { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // Init test keyring service. + dir := t.TempDir() + keyring := NewService(tt.dataStore(dir), nil) + + // Add an item and save. + err := keyring.AddItem(KeyValueItem{Key: "foo", Value: "bar"}) + require.NoError(t, err) + err = keyring.Save() + require.NoError(t, err) + + // Try adding another item after save. + err = keyring.AddItem(KeyValueItem{Key: "bar", Value: "buz"}) + require.NoError(t, err) + err = keyring.Save() + require.NoError(t, err) + + // Check the content of the keyring. + assertKeyringVals := func() { + val1, err := keyring.GetForKey("foo") + assert.NoError(t, err) + assert.Equal(t, "bar", val1.Value.(string)) + + val2, err := keyring.GetForKey("bar") + assert.NoError(t, err) + assert.Equal(t, "buz", val2.Value.(string)) + } + assertKeyringVals() + + // Try to recreate a service and make sure that the content is definitely loaded from the disk. + keyring = NewService(tt.dataStore(dir), nil) + // Check the content of the keyring. + assertKeyringVals() + }) + } +} diff --git a/test/testdata/basic.txtar b/test/testdata/basic.txtar index 94ed685..f86235b 100644 --- a/test/testdata/basic.txtar +++ b/test/testdata/basic.txtar @@ -89,4 +89,24 @@ exec launchr keyring:purge # Final Verification: Ensure complete cleanup # Verify that the encrypted keyring file no longer exists -! exists .launchr/keyring.yaml.age \ No newline at end of file +! exists .launchr/keyring.yaml.age + +# Test 7: Malformed Keyring File Handling +# ----------------------------------------------------------------------------- +# Test the application's behavior when encountering a corrupted/malformed keyring file +# This ensures graceful error handling and fallback to empty keyring behavior +cp broken-keyring.yaml.age .launchr/keyring.yaml.age +exec launchr keyring:set foo bar +stdout 'Keyring file is malformed\. It will be treated as an empty file\.' + +# Test 8: Incorrect Passphrase Handling +# ----------------------------------------------------------------------------- +# Test the application's behavior when an incorrect passphrase is provided +# This ensures proper authentication and error messaging for keyring access +! exec launchr --keyring-passphrase=123456 keyring:list +stdout 'the given passphrase is incorrect' + +# Test Data: Simulated corrupted keyring file +# This file contains plain text instead of expected encrypted age content +-- broken-keyring.yaml.age -- +this is a plain text when an age content is expected diff --git a/yaml.go b/yaml.go index 786f7f5..e116a20 100644 --- a/yaml.go +++ b/yaml.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/launchrctl/launchr" "gopkg.in/yaml.v3" ) @@ -47,12 +48,19 @@ func (s *dataStoreYaml) load() error { err = dec.Decode(&strg) // Yaml library returns io.EOF for an empty file. if err != nil && err != io.EOF { + // We check directly text because yaml library doesn't wrap errors. + // Check if the file has the valid content. if strings.Contains(err.Error(), ErrKeyringMalformed.Error()) { // The keyring is malformed, treat it as new. + launchr.Term().Warning().Println("Keyring file is malformed. It will be treated as an empty file.") s.file.Lock() s.loaded = true return nil } + // Check if the passphrase was correct. + if strings.Contains(err.Error(), ErrIncorrectPass.Error()) { + return ErrIncorrectPass + } return err } s.data = strg @@ -207,12 +215,7 @@ func (s *dataStoreYaml) CleanStorage(item SecretItem) error { // Exists implements DataStore, checks if keyring exists in persistent storage. func (s *dataStoreYaml) Exists() bool { - ageStorage, ok := s.file.(*ageFile) - if !ok { - panic("impossible type assertion") - } - - info, err := os.Stat(ageStorage.file.fname) + info, err := s.file.Stat() if os.IsNotExist(err) { return false } From ae98a36e14123326318a958bec2a41d366fb783d Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Thu, 2 Oct 2025 00:02:06 +0200 Subject: [PATCH 6/9] Update to stable dependencies. --- go.mod | 8 +++----- go.sum | 52 ++++++++----------------------------------------- test/ts_test.go | 2 +- 3 files changed, 12 insertions(+), 50 deletions(-) diff --git a/go.mod b/go.mod index 9c1fa11..df2e43a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( filippo.io/age v1.2.1 - github.com/launchrctl/launchr v0.21.3-0.20250928195706-4543b7a29604 + github.com/launchrctl/launchr v0.22.0 github.com/rogpeppe/go-internal v1.14.1 github.com/stretchr/testify v1.11.1 golang.org/x/term v0.35.0 @@ -18,6 +18,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/containerd/console v1.0.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -52,13 +53,11 @@ require ( github.com/gookit/color v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/knadh/koanf v1.5.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.17 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -81,7 +80,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pterm/pterm v0.12.81 // indirect - github.com/rivo/uniseg v0.4.7 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cobra v1.10.1 // indirect diff --git a/go.sum b/go.sum index ff81896..30a991e 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= @@ -120,60 +122,34 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= -github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= -github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= -github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= -github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= -github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= -github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= -github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= -github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= -github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= -github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= -github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= -github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= -github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= -github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= -github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= -github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= -github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= -github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= -github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= -github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= -github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= -github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= -github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= -github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= -github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -276,8 +252,6 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -308,16 +282,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/launchrctl/launchr v0.21.2 h1:D53UHpal9+/Qf+2di5+WS3/9RMWPOJMqeWVtv/gqesI= -github.com/launchrctl/launchr v0.21.2/go.mod h1:C7H4FHMSjNi4fUt36rzfyE/2xSpdBdUuq7n7IPAzqEo= -github.com/launchrctl/launchr v0.21.3-0.20250927185244-6f2a4e8fdf34 h1:EiMNfnUIPt1iknHqH+SI+5McfK+MaOkXIe9XJFVeh6E= -github.com/launchrctl/launchr v0.21.3-0.20250927185244-6f2a4e8fdf34/go.mod h1:Z2BRnU5MHVRd1KS06JVcrLf/uXhvgiO0Mkmbcbc0uTo= -github.com/launchrctl/launchr v0.21.3-0.20250928195706-4543b7a29604 h1:2eKPL46IGqbqgkfCEV8PX5qJpb9n566q1ivUZeZuOLc= -github.com/launchrctl/launchr v0.21.3-0.20250928195706-4543b7a29604/go.mod h1:cLETGKQKp6WBg1uPQ2WmrHTjPcWTfseUI0R5NyODxUs= +github.com/launchrctl/launchr v0.22.0 h1:JJJgvaSBjaxwX7vfyZ+ujwt7kIXqp/V0xy5k3JgtMyU= +github.com/launchrctl/launchr v0.22.0/go.mod h1:cLETGKQKp6WBg1uPQ2WmrHTjPcWTfseUI0R5NyODxUs= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -327,10 +295,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 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.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= -github.com/mattn/go-runewidth v0.0.17/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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -436,8 +402,6 @@ github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA= github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= diff --git a/test/ts_test.go b/test/ts_test.go index eaf1b41..94322ba 100644 --- a/test/ts_test.go +++ b/test/ts_test.go @@ -21,7 +21,7 @@ func TestKeyring(t *testing.T) { t.Parallel() testscript.Run(t, testscript.Params{ Dir: "testdata", - Deadline: time.Now().Add(30 * time.Second), + Deadline: time.Now().Add(60 * time.Second), RequireExplicitExec: true, RequireUniqueNames: true, From 4ed134aa1718db6f03df6cce5429fd46a4bb873d Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Thu, 2 Oct 2025 00:39:52 +0200 Subject: [PATCH 7/9] Skip macos-amd64 --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4a60868..60e9935 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,3 +30,5 @@ jobs: tests: name: 🛡 Testing Suite uses: launchrctl/launchr/.github/workflows/test-suite.yaml@main + with: + skip-platforms: 'macos-amd64' # tty test is failing on macos-amd64 runner for some reason, not a priority. From 99c505c553f08357fe8aaed454d07460e960bcf6 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Thu, 2 Oct 2025 00:45:47 +0200 Subject: [PATCH 8/9] Skip macos-amd64 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 60e9935..444a5c9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,4 +31,4 @@ jobs: name: 🛡 Testing Suite uses: launchrctl/launchr/.github/workflows/test-suite.yaml@main with: - skip-platforms: 'macos-amd64' # tty test is failing on macos-amd64 runner for some reason, not a priority. + skip-platforms: 'macos-amd64' # tty test is failing on macos-amd64 runner for some reason, not a priority From 2e1445e9cdeae939ba3269437139796a90a6d431 Mon Sep 17 00:00:00 2001 From: Aleksandr Britvin Date: Thu, 2 Oct 2025 01:08:58 +0200 Subject: [PATCH 9/9] Revert --- .github/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 444a5c9..4a60868 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,5 +30,3 @@ jobs: tests: name: 🛡 Testing Suite uses: launchrctl/launchr/.github/workflows/test-suite.yaml@main - with: - skip-platforms: 'macos-amd64' # tty test is failing on macos-amd64 runner for some reason, not a priority