diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 44f1021..beaa98a 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -18,7 +18,7 @@ jobs: runs-on: "['default']" language: "['go']" go-check: true - go-version: "['1.23']" + go-version: "['1.25']" node-check: false # node-version : "['node']" # fail-fast: false diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..d7b9e69 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,103 @@ +name: Release binaries + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + build-and-release: + name: Build and attach binaries + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + - goos: windows + goarch: amd64 + - goos: windows + goarch: arm64 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25.x" + + - name: Verify build + run: | + go version + go mod download + go build ./... + + - name: Build binary + env: + CGO_ENABLED: "0" + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + BIN_NAME=cloudctl + OUT_DIR=dist + mkdir -p "${OUT_DIR}" + if [ "${GOOS}" = "windows" ]; then + OUT_BIN="${OUT_DIR}/${BIN_NAME}_${GOOS}_${GOARCH}.exe" + else + OUT_BIN="${OUT_DIR}/${BIN_NAME}_${GOOS}_${GOARCH}" + fi + echo "Building ${OUT_BIN}" + go build -trimpath -ldflags="-s -w" -o "${OUT_BIN}" ./cmd + + - name: Package artifact + run: | + set -euo pipefail + BIN_NAME=cloudctl + OUT_DIR=dist + ARCHIVE_DIR=pkg + mkdir -p "${ARCHIVE_DIR}" + + FILE_BASE="${BIN_NAME}_${{ matrix.goos }}_${{ matrix.goarch }}" + if [ "${{ matrix.goos }}" = "windows" ]; then + BIN_PATH="${OUT_DIR}/${FILE_BASE}.exe" + ARCHIVE_PATH="${ARCHIVE_DIR}/${FILE_BASE}.zip" + (cd "${OUT_DIR}" && zip -9 "../${ARCHIVE_PATH}" "${FILE_BASE}.exe") + else + BIN_PATH="${OUT_DIR}/${FILE_BASE}" + ARCHIVE_PATH="${ARCHIVE_DIR}/${FILE_BASE}.tar.gz" + (cd "${OUT_DIR}" && tar -czf "../${ARCHIVE_PATH}" "${FILE_BASE}") + fi + + echo "ARCHIVE_PATH=${ARCHIVE_PATH}" >> $GITHUB_ENV + + - name: Create GitHub Release (if not exists) + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + draft: false + prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Upload artifact to release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + files: ${{ env.ARCHIVE_PATH }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..97eb5b7 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,67 @@ +name: Tests + +on: + pull_request: + push: + branches: [ main ] + +jobs: + unit: + name: Unit tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + + - name: Show Go env + run: | + go version + go env + + - name: Tidy modules + run: make tidy + + - name: Run unit tests + run: make test + + e2e: + name: E2E tests (k3d) + needs: unit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + + - name: Install kubectl + uses: azure/setup-kubectl@v4 + with: + version: 'v1.29.0' # any modern kubectl compatible with k3d's default + + - name: Install k3d + run: | + curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash + k3d version + + - name: Build binary + run: make build + + - name: Run E2E tests + env: + # Optional: customize e2e settings via Makefile variables if needed + # E2E_CLUSTER_NAME: cloudctl-e2e + # E2E_TAGS: e2e + # E2E_KUBECONFIG: ${{ github.workspace }}/e2e/e2e-kubeconfig + # E2E_BIN: ${{ github.workspace }}/bin/cloudctl + # Increase verbosity by adding GOFLAGS=-v or similar if desired + GOFLAGS: "" + run: make e2e diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9083d52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/e2e/e2e-kubeconfig +/e2e/cloudctl diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bbfa37e --- /dev/null +++ b/Makefile @@ -0,0 +1,133 @@ +# Project variables +BIN ?= cloudctl +PKG ?= github.com/cloudoperators/cloudctl +CMD_PKG ?= . +BUILD_DIR ?= bin + +# Versioning (overridable) +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) +BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) + +# Go options +GO ?= go +GOFLAGS ?= +TAGS ?= +LDFLAGS ?= -X '$(PKG)/cmd.Version=$(VERSION)' -X '$(PKG)/cmd.GitCommit=$(GIT_COMMIT)' -X '$(PKG)/cmd.BuildDate=$(BUILD_DATE)' +GCFLAGS ?= +ASMFLAGS ?= +RACE ?= + +# E2E options +E2E_CLUSTER_NAME ?= cloudctl-e2e +E2E_KUBECONFIG ?= $(CURDIR)/e2e/e2e-kubeconfig +E2E_PKG ?= ./e2e +E2E_TAGS ?= e2e +# Use absolute path so tests can find the binary regardless of working directory +E2E_BIN ?= $(CURDIR)/$(BUILD_DIR)/$(BIN) + +# Extra args +ARGS ?= + +# Derived +BUILD_FLAGS := $(if $(RACE),-race,) $(GOFLAGS) -tags '$(TAGS)' -ldflags "$(LDFLAGS)" -gcflags '$(GCFLAGS)' -asmflags '$(ASMFLAGS)' + +.PHONY: all build install run test cover cover-html fmt vet tidy clean version print-vars \ + e2e-up e2e-down e2e-build e2e-test e2e + +all: build + +build: + @mkdir -p $(BUILD_DIR) + $(GO) build $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BIN) $(CMD_PKG) + +install: + $(GO) install $(BUILD_FLAGS) $(CMD_PKG) + +run: + $(GO) run $(BUILD_FLAGS) $(CMD_PKG) $(ARGS) + +test: + $(GO) test $(GOFLAGS) -tags '$(TAGS)' $(if $(RACE),-race,) ./... + +cover: + $(GO) test $(GOFLAGS) -tags '$(TAGS)' -coverprofile=coverage.out ./... + @echo "Coverage summary:" + $(GO) tool cover -func=coverage.out + +cover-html: cover + $(GO) tool cover -html=coverage.out -o coverage.html + @echo "Open coverage.html in your browser." + +fmt: + $(GO) fmt ./... + +fmt-check: + @echo "Checking formatting with gofmt and gofumpt..." + @set -e; \ + out1="$$(gofmt -l .)"; \ + out2="$$( $(GO) run mvdan.cc/gofumpt@latest -l .)"; \ + files=""; \ + if [ -n "$$out1" ]; then files="$$files\n$$out1"; fi; \ + if [ -n "$$out2" ]; then files="$$files\n$$out2"; fi; \ + if [ -n "$$files" ]; then \ + echo "The following files need formatting:"; \ + printf "%b\n" "$$files" | sed '/^$$/d' | sort -u; \ + echo "Run: make fmt"; \ + exit 1; \ + fi + +vet: + $(GO) vet ./... + +tidy: + $(GO) mod tidy + +clean: + @rm -rf $(BUILD_DIR) coverage.out coverage.html ./bin/cloudctl ./e2e/cloudctl ./e2e/e2e-kubeconfig + +version: + @echo "Version: $(VERSION)" + @echo "Git commit: $(GIT_COMMIT)" + @echo "Build date: $(BUILD_DATE)" + +print-vars: + @echo "BIN=$(BIN)" + @echo "PKG=$(PKG)" + @echo "CMD_PKG=$(CMD_PKG)" + @echo "BUILD_DIR=$(BUILD_DIR)" + @echo "VERSION=$(VERSION)" + @echo "GIT_COMMIT=$(GIT_COMMIT)" + @echo "BUILD_DATE=$(BUILD_DATE)" + @echo "GOFLAGS=$(GOFLAGS)" + @echo "TAGS=$(TAGS)" + @echo "LDFLAGS=$(LDFLAGS)" + @echo "RACE=$(RACE)" + @echo "E2E_CLUSTER_NAME=$(E2E_CLUSTER_NAME)" + @echo "E2E_KUBECONFIG=$(E2E_KUBECONFIG)" + @echo "E2E_BIN=$(E2E_BIN)" + @echo "E2E_PKG=$(E2E_PKG)" + @echo "E2E_TAGS=$(E2E_TAGS)" + +# --- E2E --- + +e2e-up: + @./e2e/k3d-up.sh "$(E2E_CLUSTER_NAME)" "$(E2E_KUBECONFIG)" + +e2e-down: + @./e2e/k3d-down.sh "$(E2E_CLUSTER_NAME)" + +e2e-build: build + @echo "Using E2E binary: $(E2E_BIN)" + +# Ensure kubeconfig exists before running tests; if not, bring the cluster up and write it. +e2e-test: e2e-build + @if [ ! -f "$(E2E_KUBECONFIG)" ]; then \ + echo "Kubeconfig $(E2E_KUBECONFIG) not found; creating via k3d-up..."; \ + ./e2e/k3d-up.sh "$(E2E_CLUSTER_NAME)" "$(E2E_KUBECONFIG)"; \ + fi + @echo "Running e2e tests against kubeconfig: $(E2E_KUBECONFIG)" + E2E_KUBECONFIG="$(E2E_KUBECONFIG)" E2E_BIN="$(E2E_BIN)" $(GO) test -v -tags '$(E2E_TAGS)' $(E2E_PKG) + +# Convenience: bring up cluster, run tests, then tear down. +e2e: e2e-up e2e-test e2e-down diff --git a/README.md b/README.md index d7a5007..3352f43 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,40 @@ # cloudctl -## About this project - -Unified Kubernetes cli for the cloud. +Unified Kubernetes CLI for the cloud. ``` cloudctl is a command line interface that helps: - 1) Fetch and merge kubeconfigs from central Greenhouse cluster + 1) Fetch and merge kubeconfigs from the central Greenhouse cluster into your local kubeconfig + 2) Sync contexts and credentials for seamless kubectl usage + 3) Inspect the Kubernetes version of a target cluster + 4) Print the cloudctl version and build information + +Examples: + - Merge/refresh kubeconfigs from Greenhouse: + cloudctl sync + + - Show Kubernetes version for a specific context: + cloudctl cluster-version --context my-cluster + + - Show cloudctl version: + cloudctl version Usage: cloudctl [command] Available Commands: - completion Generate the autocompletion script for the specified shell - help Help about any command - sync Fetches remote kubeconfigs from Greenhouse cluster and merges them into your local config + cluster-version Prints the cluster version of the context in kubeconfig + completion Generate the autocompletion script for the specified shell + help Help about any command + sync Fetches kubeconfigs of remote clusters from Greenhouse cluster and merges them into your local config + version Print the cloudctl version information Flags: -h, --help help for cloudctl + +Use "cloudctl [command] --help" for more information about a command. ``` ## Requirements and Setup diff --git a/cmd/cluster-version.go b/cmd/cluster-version.go index 6d04c7c..db6fb56 100644 --- a/cmd/cluster-version.go +++ b/cmd/cluster-version.go @@ -31,7 +31,6 @@ var ( ) func runClusterVersion(cmd *cobra.Command, args []string) error { - cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { return fmt.Errorf("failed to build kubeconfig: %w", err) @@ -83,10 +82,6 @@ func hasAuth(cfg *rest.Config) bool { if len(cfg.TLSClientConfig.CertData) > 0 || cfg.TLSClientConfig.CertFile != "" { return true } - if cfg.ExecProvider != nil { - return true - } - if cfg.ExecProvider != nil { return true } diff --git a/cmd/cluster-version_test.go b/cmd/cluster-version_test.go new file mode 100644 index 0000000..0581e03 --- /dev/null +++ b/cmd/cluster-version_test.go @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "crypto/tls" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/rest" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func TestHasAuth(t *testing.T) { + g := NewWithT(t) + + g.Expect(hasAuth(&rest.Config{})).To(BeFalse(), "no auth should be detected") + + g.Expect(hasAuth(&rest.Config{BearerToken: "x"})).To(BeTrue(), "bearer token should be detected") + g.Expect(hasAuth(&rest.Config{BearerTokenFile: "/tmp/token"})).To(BeTrue(), "bearer token file should be detected") + g.Expect(hasAuth(&rest.Config{Username: "u", Password: "p"})).To(BeTrue(), "basic auth should be detected") + g.Expect(hasAuth(&rest.Config{TLSClientConfig: rest.TLSClientConfig{CertData: []byte("cert")}})).To(BeTrue(), "client cert data should be detected") + g.Expect(hasAuth(&rest.Config{TLSClientConfig: rest.TLSClientConfig{CertFile: "/tmp/cert"}})).To(BeTrue(), "client cert file should be detected") + g.Expect(hasAuth(&rest.Config{ExecProvider: &clientcmdapi.ExecConfig{Command: "kubelogin"}})).To(BeTrue(), "exec provider should be detected") + + g.Expect(hasAuth(&rest.Config{ + AuthProvider: &clientcmdapi.AuthProviderConfig{Config: map[string]string{"id-token": "t"}}, + })).To(BeTrue(), "auth provider with id-token should be detected") + + g.Expect(hasAuth(&rest.Config{ + AuthProvider: &clientcmdapi.AuthProviderConfig{Config: map[string]string{"refresh-token": "r"}}, + })).To(BeFalse(), "auth provider without id-token should not be detected") +} + +func TestGetUnauthenticatedVersion_OK(t *testing.T) { + g := NewWithT(t) + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/version" { + http.NotFound(w, r) + return + } + _ = json.NewEncoder(w).Encode(&version.Info{GitVersion: "v1.28.3"}) + })) + defer srv.Close() + + cfg := &rest.Config{Host: srv.URL, TLSClientConfig: rest.TLSClientConfig{Insecure: true}} + v, err := getUnauthenticatedVersion(cfg) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(v).ToNot(BeNil()) + g.Expect(v.GitVersion).To(Equal("v1.28.3")) +} + +func TestGetUnauthenticatedVersion_StatusError(t *testing.T) { + g := NewWithT(t) + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "forbidden", http.StatusForbidden) + })) + defer srv.Close() + + cfg := &rest.Config{Host: srv.URL, TLSClientConfig: rest.TLSClientConfig{Insecure: true}} + _, err := getUnauthenticatedVersion(cfg) + g.Expect(err).To(HaveOccurred()) +} + +func TestGetUnauthenticatedVersion_InsecureTLS(t *testing.T) { + g := NewWithT(t) + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(&version.Info{GitVersion: "v0.0.0"}) + })) + defer srv.Close() + + cfgInsecure := &rest.Config{Host: srv.URL, TLSClientConfig: rest.TLSClientConfig{Insecure: true}} + _, err := getUnauthenticatedVersion(cfgInsecure) + g.Expect(err).ToNot(HaveOccurred()) + + cfgStrict := &rest.Config{Host: srv.URL, TLSClientConfig: rest.TLSClientConfig{Insecure: false}} + _, err = getUnauthenticatedVersion(cfgStrict) + g.Expect(err).To(HaveOccurred()) + + _ = tls.Config{} // keep import used +} diff --git a/cmd/root.go b/cmd/root.go index bc592ab..f940a89 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,23 +4,54 @@ package cmd import ( + "context" + "github.com/spf13/cobra" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" ) var rootCmd = &cobra.Command{ Use: "cloudctl", - Short: "A CLI tool to access Greenhouse clusters", + Short: "Manage and access Kubernetes clusters via Greenhouse", Long: `cloudctl is a command line interface that helps: - 1) Fetch and merge kubeconfigs from central Greenhouse cluster`, + 1) Fetch and merge kubeconfigs from the central Greenhouse cluster into your local kubeconfig + 2) Sync contexts and credentials for seamless kubectl usage + 3) Inspect the Kubernetes version of a target cluster + 4) Print the cloudctl version and build information + +Examples: + - Merge/refresh kubeconfigs from Greenhouse: + cloudctl sync + + - Show Kubernetes version for a specific context: + cloudctl cluster-version --context my-cluster + + - Show cloudctl version: + cloudctl version`, } -func Execute() error { - return rootCmd.Execute() +// Execute runs the CLI with the provided context. +func Execute(ctx context.Context) error { + return rootCmd.ExecuteContext(ctx) } func init() { // Add subcommands here rootCmd.AddCommand(syncCmd) rootCmd.AddCommand(clusterVersionCmd) + rootCmd.AddCommand(versionCmd) +} + +// configWithContext builds a rest.Config for the specified context name from the given kubeconfig path. +func configWithContext(contextName, kubeconfigPath string) (*rest.Config, error) { + loadingRules := &clientcmd.ClientConfigLoadingRules{ + ExplicitPath: kubeconfigPath, + } + overrides := &clientcmd.ConfigOverrides{ + CurrentContext: contextName, + } + cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) + return cc.ClientConfig() } diff --git a/cmd/sync.go b/cmd/sync.go index 7bec6f5..cd76118 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -13,11 +13,10 @@ import ( "maps" "strings" - "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" + "github.com/cloudoperators/greenhouse/api/v1alpha1" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/rest" - clientcmd "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -43,16 +42,13 @@ func init() { syncCmd.Flags().BoolVar(&mergeIdenticalUsers, "merge-identical-users", true, "merge identical user information in kubeconfig file so that you only login once for the clusters that share the same auth info") } -var ( - syncCmd = &cobra.Command{ - Use: "sync", - Short: "Fetches kubeconfigs of remote clusters from Greenhouse cluster and merges them into your local config", - RunE: runSync, - } -) +var syncCmd = &cobra.Command{ + Use: "sync", + Short: "Fetches kubeconfigs of remote clusters from Greenhouse cluster and merges them into your local config", + RunE: runSync, +} func runSync(cmd *cobra.Command, args []string) error { - centralConfig, err := clientcmd.BuildConfigFromFlags("", greenhouseClusterKubeconfig) if err != nil { return fmt.Errorf("failed to build greenhouse kubeconfig: %w", err) @@ -116,7 +112,7 @@ func runSync(cmd *cobra.Command, args []string) error { err = mergeKubeconfig(localConfig, serverConfig) if err != nil { - return fmt.Errorf("failed to merge ClusterKubeconfig: %w", err) + return fmt.Errorf(`failed to merge ClusterKubeconfig: %w`, err) } err = writeConfig(localConfig, remoteClusterKubeconfig) @@ -134,9 +130,8 @@ func buildIncomingKubeconfig(items []v1alpha1.ClusterKubeconfig) (*clientcmdapi. kubeconfig := clientcmdapi.NewConfig() for _, ckc := range items { - // Assuming each ClusterKubeconfig has exactly one context, authInfo, and cluster. - if len(ckc.Spec.Kubeconfig.Contexts) > 0 { - ctxItem := ckc.Spec.Kubeconfig.Contexts[0] + // Add all contexts + for _, ctxItem := range ckc.Spec.Kubeconfig.Contexts { kubeconfig.Contexts[ctxItem.Name] = &clientcmdapi.Context{ Cluster: ctxItem.Context.Cluster, AuthInfo: ctxItem.Context.AuthInfo, @@ -144,17 +139,19 @@ func buildIncomingKubeconfig(items []v1alpha1.ClusterKubeconfig) (*clientcmdapi. } } - if len(ckc.Spec.Kubeconfig.AuthInfo) > 0 { - authItem := ckc.Spec.Kubeconfig.AuthInfo[0] + // Add all users (auth infos) + for _, authItem := range ckc.Spec.Kubeconfig.AuthInfo { + // Preserve the same data shape; exclude nothing here (merging will handle dedupe) + authProvider := authItem.AuthInfo.AuthProvider kubeconfig.AuthInfos[authItem.Name] = &clientcmdapi.AuthInfo{ ClientCertificateData: authItem.AuthInfo.ClientCertificateData, ClientKeyData: authItem.AuthInfo.ClientKeyData, - AuthProvider: &authItem.AuthInfo.AuthProvider, + AuthProvider: &authProvider, } } - if len(ckc.Spec.Kubeconfig.Clusters) > 0 { - clusterItem := ckc.Spec.Kubeconfig.Clusters[0] + // Add all clusters + for _, clusterItem := range ckc.Spec.Kubeconfig.Clusters { kubeconfig.Clusters[clusterItem.Name] = &clientcmdapi.Cluster{ Server: clusterItem.Cluster.Server, @@ -273,7 +270,6 @@ func generateAuthInfoKey(authInfo *clientcmdapi.AuthInfo) string { } func mergeKubeconfig(localConfig *clientcmdapi.Config, serverConfig *clientcmdapi.Config) error { - // Merge Clusters for serverName, serverCluster := range serverConfig.Clusters { managedName := managedNameFunc(serverName) @@ -305,7 +301,7 @@ func mergeKubeconfig(localConfig *clientcmdapi.Config, serverConfig *clientcmdap // Generate a unique key based on AuthInfo excluding id-token and refresh-token uniqueKey := generateAuthInfoKey(serverAuth) hash := sha256.Sum256([]byte(uniqueKey)) - hashString := hex.EncodeToString(hash[:])[:16] // Using first 16 chars for brevity + hashString := hex.EncodeToString(hash[:])[:16] // Using the first 16 chars for brevity managedAuthName = fmt.Sprintf("%s:auth-%s", prefix, hashString) // **Merge AuthInfo to preserve id-token and refresh-token** @@ -335,7 +331,7 @@ func mergeKubeconfig(localConfig *clientcmdapi.Config, serverConfig *clientcmdap // Merge Contexts for serverName, serverCtx := range serverConfig.Contexts { - managedName := serverName // it is same for context + managedName := serverName // it is the same for context var managedAuthInfoName string if mergeIdenticalUsers { @@ -349,7 +345,7 @@ func mergeKubeconfig(localConfig *clientcmdapi.Config, serverConfig *clientcmdap var existsInMap bool managedAuthInfoName, existsInMap = authInfoMap[uniqueKey] if !existsInMap { - // This should not happen as all AuthInfos should have been processed + // This should not happen as all AuthInfos should have been processed. // However, to be safe, generate a new managedAuthName hash := sha256.Sum256([]byte(uniqueKey)) hashString := hex.EncodeToString(hash[:])[:16] @@ -470,6 +466,10 @@ func mergeAuthInfo(serverAuth, localAuth *clientcmdapi.AuthInfo) *clientcmdapi.A // Preserve id-token and refresh-token from localAuth if localAuth.AuthProvider != nil && mergedAuth.AuthProvider != nil { + // Ensure the merged config map is initialized to avoid panics on assignment + if mergedAuth.AuthProvider.Config == nil { + mergedAuth.AuthProvider.Config = make(map[string]string) + } if idToken, exists := localAuth.AuthProvider.Config["id-token"]; exists { mergedAuth.AuthProvider.Config["id-token"] = idToken } @@ -478,7 +478,7 @@ func mergeAuthInfo(serverAuth, localAuth *clientcmdapi.AuthInfo) *clientcmdapi.A } } - // Additionally, preserve other fields if necessary + // Additionally, preserve other fields if necessary. // For example, ClientCertificateData and ClientKeyData are already handled return mergedAuth @@ -511,11 +511,3 @@ func extensionRaw(m map[string]runtime.Object, name string) []byte { return bytes.TrimSpace(b) } } - -func configWithContext(context, kubeconfigPath string) (*rest.Config, error) { - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, - &clientcmd.ConfigOverrides{ - CurrentContext: context, - }).ClientConfig() -} diff --git a/cmd/sync_test.go b/cmd/sync_test.go new file mode 100644 index 0000000..b1ad203 --- /dev/null +++ b/cmd/sync_test.go @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "bytes" + "testing" + + . "github.com/onsi/gomega" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +// sync_merge_test.go + +func TestManagedNameHelpers(t *testing.T) { + g := NewWithT(t) + + orig := prefix + prefix = "cloudctl" + t.Cleanup(func() { prefix = orig }) + + name := "mycluster" + mn := managedNameFunc(name) + g.Expect(mn).To(Equal("cloudctl:mycluster")) + g.Expect(isManaged(mn)).To(BeTrue()) + g.Expect(isManaged(name)).To(BeFalse()) + + g.Expect(unmanagedNameFunc(mn)).To(Equal(name)) +} + +func TestFilterAuthProviderConfig(t *testing.T) { + g := NewWithT(t) + + in := map[string]string{ + "id-token": "secret", + "refresh-token": "secret2", + "client-id": "cid", + "client-secret": "csec", + "auth-request-extra-params": "aud=foo", + "extra-scopes": "groups,offline_access", + "keep": "x", + } + out := filterAuthProviderConfig(in) + + g.Expect(out).ToNot(HaveKey("id-token")) + g.Expect(out).ToNot(HaveKey("refresh-token")) + g.Expect(out).To(HaveKeyWithValue("client-id", "cid")) + g.Expect(out).To(HaveKeyWithValue("client-secret", "csec")) + g.Expect(out).To(HaveKeyWithValue("auth-request-extra-params", "aud=foo")) + g.Expect(out).To(HaveKeyWithValue("extra-scopes", "groups,offline_access")) + g.Expect(out).To(HaveKeyWithValue("keep", "x")) +} + +func TestAuthInfoEqual_IgnoresTokens(t *testing.T) { + g := NewWithT(t) + + a := &clientcmdapi.AuthInfo{ + AuthProvider: &clientcmdapi.AuthProviderConfig{ + Name: "oidc", + Config: map[string]string{ + "client-id": "cid", + "client-secret": "csec", + "id-token": "tokA", + "refresh-token": "refA", + }, + }, + } + b := &clientcmdapi.AuthInfo{ + AuthProvider: &clientcmdapi.AuthProviderConfig{ + Name: "oidc", + Config: map[string]string{ + "client-id": "cid", + "client-secret": "csec", + "id-token": "tokB", + "refresh-token": "refB", + }, + }, + } + g.Expect(authInfoEqual(a, b)).To(BeTrue(), "token differences should be ignored") +} + +func TestAuthInfoEqual_DiffCerts(t *testing.T) { + g := NewWithT(t) + + a := &clientcmdapi.AuthInfo{ + ClientCertificateData: []byte("certA"), + ClientKeyData: []byte("keyA"), + } + b := &clientcmdapi.AuthInfo{ + ClientCertificateData: []byte("certB"), + ClientKeyData: []byte("keyA"), + } + g.Expect(authInfoEqual(a, b)).To(BeFalse(), "different certs should not be equal") +} + +func TestGenerateAuthInfoKey_OIDC(t *testing.T) { + g := NewWithT(t) + + a := &clientcmdapi.AuthInfo{ + AuthProvider: &clientcmdapi.AuthProviderConfig{ + Name: "oidc", + Config: map[string]string{ + "client-id": "cid", + "client-secret": "csec", + "auth-request-extra-params": "aud=foo", + "extra-scopes": "groups,offline_access", + "id-token": "tokA", + "refresh-token": "refA", + }, + }, + } + b := &clientcmdapi.AuthInfo{ + AuthProvider: &clientcmdapi.AuthProviderConfig{ + Name: "oidc", + Config: map[string]string{ + "client-id": "cid", + "client-secret": "csec", + "auth-request-extra-params": "aud=foo", + "extra-scopes": "groups,offline_access", + "id-token": "tokB", + "refresh-token": "refB", + }, + }, + } + ka := generateAuthInfoKey(a) + kb := generateAuthInfoKey(b) + g.Expect(ka).To(Equal(kb), "tokens must not affect dedupe key") +} + +func TestGenerateAuthInfoKey_CertBased(t *testing.T) { + g := NewWithT(t) + + a := &clientcmdapi.AuthInfo{ + ClientCertificateData: []byte("certA"), + ClientKeyData: []byte("keyA"), + } + b := &clientcmdapi.AuthInfo{ + ClientCertificateData: []byte("certA"), + ClientKeyData: []byte("keyA"), + } + ka := generateAuthInfoKey(a) + kb := generateAuthInfoKey(b) + + g.Expect(ka).To(Equal(kb)) + g.Expect(bytes.HasPrefix([]byte(ka), []byte("cert:"))).To(BeTrue(), "cert-based key should have cert: prefix") +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..34f3dd8 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "encoding/json" + "fmt" + "runtime" + + "github.com/spf13/cobra" +) + +var ( + // These can be overridden at build time via -ldflags + // -ldflags="-X 'github.com/cloudoperators/cloudctl/cmd.Version=v1.2.3' -X 'github.com/cloudoperators/cloudctl/cmd.GitCommit=abcdef' -X 'github.com/cloudoperators/cloudctl/cmd.BuildDate=2025-08-22T12:34:56Z'" + Version = "dev" + GitCommit = "unknown" + BuildDate = "unknown" +) + +type versionInfo struct { + Version string `json:"version"` + GitCommit string `json:"gitCommit"` + BuildDate string `json:"buildDate"` + GoVersion string `json:"goVersion"` + Compiler string `json:"compiler"` + Platform string `json:"platform"` +} + +var ( + versionShort bool + versionJSON bool +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the cloudctl version information", + RunE: func(cmd *cobra.Command, args []string) error { + info := versionInfo{ + Version: Version, + GitCommit: GitCommit, + BuildDate: BuildDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + } + + if versionJSON { + b, err := json.MarshalIndent(info, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil + } + + if versionShort { + fmt.Println(info.Version) + return nil + } + + fmt.Printf("cloudctl %s\n", info.Version) + fmt.Printf(" git commit: %s\n", info.GitCommit) + fmt.Printf(" build date: %s\n", info.BuildDate) + fmt.Printf(" go: %s %s %s/%s\n", info.GoVersion, info.Compiler, runtime.GOOS, runtime.GOARCH) + return nil + }, +} + +func init() { + versionCmd.Flags().BoolVar(&versionShort, "short", false, "print only the version number") + versionCmd.Flags().BoolVar(&versionJSON, "json", false, "print version information as JSON") +} diff --git a/e2e/cluster-version_test.go b/e2e/cluster-version_test.go new file mode 100644 index 0000000..7e223c7 --- /dev/null +++ b/e2e/cluster-version_test.go @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build e2e + +package e2e + +import ( + "fmt" + "regexp" + "strings" + "testing" + "time" + + . "github.com/onsi/gomega" +) + +func TestE2E_ClusterVersion(t *testing.T) { + g := NewWithT(t) + + kubeconfig := resolveKubeconfig(t) + requireFileG(g, kubeconfig) + + bin := resolveBin(t) + requireFileG(g, bin) + + re := regexp.MustCompile(`^\d+(\.\d+)*$`) + + g.Eventually(func() error { + stdout, stderr, err := runCmd(bin, "cluster-version", "-k", kubeconfig) + if err != nil { + return fmt.Errorf("cluster-version error: %v (stderr: %s)", err, stderr) + } + out := strings.TrimSpace(stdout) + if out == "" { + return fmt.Errorf("empty cluster-version output") + } + if !re.MatchString(out) { + return fmt.Errorf("unexpected version format: %q", out) + } + return nil + }, time.Minute, 3*time.Second).Should(Succeed()) +} diff --git a/e2e/k3d-down.sh b/e2e/k3d-down.sh new file mode 100755 index 0000000..399a039 --- /dev/null +++ b/e2e/k3d-down.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +CLUSTER_NAME="${1:-cloudctl-e2e}" + +command -v k3d >/dev/null 2>&1 || { echo "k3d is required. Install from https://k3d.io/." >&2; exit 1; } + +if k3d cluster list | awk 'NR>1 {print $1}' | grep -qx "$CLUSTER_NAME"; then + echo "Deleting k3d cluster '$CLUSTER_NAME'..." + k3d cluster delete "$CLUSTER_NAME" +else + echo "Cluster '$CLUSTER_NAME' not found. Nothing to delete." +fi diff --git a/e2e/k3d-up.sh b/e2e/k3d-up.sh new file mode 100755 index 0000000..50875a4 --- /dev/null +++ b/e2e/k3d-up.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +CLUSTER_NAME="${1:-cloudctl-e2e}" +OUT_KUBECONFIG="${2:-./tmp/e2e-kubeconfig}" + +command -v k3d >/dev/null 2>&1 || { echo "k3d is required. Install from https://k3d.io/." >&2; exit 1; } +command -v kubectl >/dev/null 2>&1 || { echo "kubectl is required. Install kubectl." >&2; exit 1; } + +mkdir -p "$(dirname "$OUT_KUBECONFIG")" + +# Create cluster if it doesn't exist +if k3d cluster list | awk 'NR>1 {print $1}' | grep -qx "$CLUSTER_NAME"; then + echo "Cluster '$CLUSTER_NAME' already exists." +else + echo "Creating k3d cluster '$CLUSTER_NAME'..." + k3d cluster create "$CLUSTER_NAME" --wait +fi + +echo "Writing kubeconfig to $OUT_KUBECONFIG" +# IMPORTANT: use 'k3d kubeconfig get' to emit YAML, not 'write' (which prints a path) +k3d kubeconfig get "$CLUSTER_NAME" > "$OUT_KUBECONFIG" +chmod 600 "$OUT_KUBECONFIG" + +echo "Cluster '$CLUSTER_NAME' is ready." +kubectl --kubeconfig "$OUT_KUBECONFIG" get nodes \ No newline at end of file diff --git a/e2e/sync_test.go b/e2e/sync_test.go new file mode 100644 index 0000000..db5aa35 --- /dev/null +++ b/e2e/sync_test.go @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build e2e + +package e2e + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/gomega" + clientcmd "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +func TestE2E_Sync(t *testing.T) { + g := NewWithT(t) + + kubeconfig := resolveKubeconfig(t) + requireFileG(g, kubeconfig) + bin := resolveBin(t) + requireFileG(g, bin) + + ns := "e2e-sync" + prefix := "e2e" + crFile := filepath.Join(os.TempDir(), "clusterkubeconfig-e2e.yaml") + + // Prefer applying the CRD from the repository path that matches the provided spec (greenhouse.sap group). + remoteCRD := "https://raw.githubusercontent.com/cloudoperators/greenhouse/refs/heads/main/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml" + + // Try local cache first (optional), otherwise fall back to the remote CRD above. + appliedCRD := false + if modDir := getModuleDir(t, "github.com/cloudoperators/greenhouse"); modDir != "" { + local := filepath.Join(modDir, "charts", "manager", "crds", "greenhouse.sap_clusterkubeconfigs.yaml") + if fi, err := os.Stat(local); err == nil && !fi.IsDir() { + if _, stderr, err := runCmd("kubectl", "--kubeconfig", kubeconfig, "apply", "-f", local); err == nil { + appliedCRD = true + } else { + t.Logf("failed applying local CRD %s: %s", local, stderr) + } + } + } + if !appliedCRD { + if _, stderr, err := runCmd("kubectl", "--kubeconfig", kubeconfig, "apply", "-f", remoteCRD); err == nil { + appliedCRD = true + } else { + t.Skipf("failed applying CRD from %s: %s", remoteCRD, stderr) + } + } + + // Wait until the CRD is established; this CRD uses greenhouse.sap + g.Eventually(func() error { + if _, _, err := runCmd("kubectl", "--kubeconfig", kubeconfig, "get", "crd", "clusterkubeconfigs.greenhouse.sap"); err == nil { + return nil + } + return fmt.Errorf("crd not found yet") + }, 90*time.Second, 3*time.Second).Should(Succeed()) + + // Demo CR aligned with the CRD schema; omit empty certificate-authority-data to satisfy byte type. + crYAML := ` +apiVersion: greenhouse.sap/v1alpha1 +kind: ClusterKubeconfig +metadata: + name: demo + namespace: ` + ns + ` +spec: + kubeconfig: + clusters: + - name: rc + cluster: + server: https://example.invalid + users: + - name: user + user: + auth-provider: + name: oidc + config: + client-id: demo + contexts: + - name: ctx1 + context: + cluster: rc + user: user + namespace: default +` + writeFile(t, crFile, crYAML) + + // Ensure namespace + if _, _, err := runCmd("kubectl", "--kubeconfig", kubeconfig, "get", "ns", ns); err != nil { + if _, stderr, err := runCmd("kubectl", "--kubeconfig", kubeconfig, "create", "ns", ns); err != nil { + t.Fatalf("create ns: %v (stderr: %s)", err, stderr) + } + } + + // Apply CR + if _, stderr, err := runCmd("kubectl", "--kubeconfig", kubeconfig, "apply", "-f", crFile); err != nil { + t.Fatalf("apply CR failed: %v (stderr: %s)", err, stderr) + } + + // Target kubeconfig file + targetKubeconfig := filepath.Join(os.TempDir(), "e2e-sync-target-kubeconfig") + createEmptyKubeconfigFile(t, targetKubeconfig) + + // Run sync + if _, stderr, err := runCmd(bin, + "sync", + "--greenhouse-cluster-kubeconfig", kubeconfig, + "--greenhouse-cluster-namespace", ns, + "--remote-cluster-kubeconfig", targetKubeconfig, + "--prefix", prefix, + ); err != nil { + t.Fatalf("sync failed: %v (stderr: %s)", err, stderr) + } + + // Validate + cfg, loadErr := clientcmd.LoadFromFile(targetKubeconfig) + g.Expect(loadErr).ToNot(HaveOccurred()) + + managedCluster := prefix + ":" + "rc" + g.Expect(cfg.Clusters).To(HaveKey(managedCluster)) + g.Expect(cfg.Contexts).To(HaveKey("ctx1")) + g.Expect(cfg.Contexts["ctx1"].Cluster).To(Equal(managedCluster)) + g.Expect(cfg.Contexts["ctx1"].AuthInfo).To(ContainSubstring(prefix + ":")) + + // Cleanup (best-effort) + _, _, _ = runCmd("kubectl", "--kubeconfig", kubeconfig, "delete", "-f", crFile, "--ignore-not-found=true") + _, _, _ = runCmd("kubectl", "--kubeconfig", kubeconfig, "delete", "ns", ns, "--ignore-not-found=true") +} + +// Helpers + +func writeFile(t *testing.T, path, content string) { + t.Helper() + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} + +func createEmptyKubeconfigFile(t *testing.T, path string) { + t.Helper() + cfg := clientcmdapi.NewConfig() + if err := clientcmd.WriteToFile(*cfg, path); err != nil { + t.Fatalf("write empty kubeconfig: %v", err) + } +} + +func getModuleDir(t *testing.T, module string) string { + t.Helper() + out, err := exec.Command("go", "list", "-m", "-json", module).Output() + if err != nil { + // Return empty when not available; caller may skip to remote + return "" + } + var m struct { + Path string + Dir string + } + if jerr := json.Unmarshal(out, &m); jerr != nil { + return "" + } + return m.Dir +} diff --git a/e2e/utils_test.go b/e2e/utils_test.go new file mode 100644 index 0000000..10a94f2 --- /dev/null +++ b/e2e/utils_test.go @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build e2e + +package e2e + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" +) + +type versionInfo struct { + Version string `json:"version"` + GitCommit string `json:"gitCommit"` + BuildDate string `json:"buildDate"` + GoVersion string `json:"goVersion"` + Compiler string `json:"compiler"` + Platform string `json:"platform"` +} + +func runCmd(bin string, args ...string) (string, string, error) { + cmd := exec.Command(bin, args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Env = append(os.Environ(), + "CLOUDCTL_TEST=1", + "KUBECONFIG="+os.Getenv("E2E_KUBECONFIG"), + ) + err := cmd.Run() + return stdout.String(), stderr.String(), err +} + +func requireFileG(g Gomega, path string) { + fi, err := os.Stat(path) + g.Expect(err).ToNot(HaveOccurred(), "stat %s", path) + g.Expect(fi.IsDir()).To(BeFalse(), "expected file, got directory: %s", path) +} + +// resolveBin attempts to find the cloudctl binary using E2E_BIN or common locations. +func resolveBin(t *testing.T) string { + t.Helper() + if v := os.Getenv("E2E_BIN"); v != "" { + if fi, err := os.Stat(v); err == nil && !fi.IsDir() { + return v + } + } + candidates := []string{ + filepath.Join(".", "bin", "cloudctl"), + filepath.Join("..", "..", "bin", "cloudctl"), + } + for _, c := range candidates { + if fi, err := os.Stat(c); err == nil && !fi.IsDir() { + return c + } + } + if p, err := exec.LookPath("cloudctl"); err == nil { + return p + } + t.Fatalf("cloudctl binary not found. Set E2E_BIN or run 'make build' to produce ./bin/cloudctl") + return "" +} + +// resolveKubeconfig attempts to find kubeconfig using E2E_KUBECONFIG or common locations. +func resolveKubeconfig(t *testing.T) string { + t.Helper() + if v := os.Getenv("E2E_KUBECONFIG"); v != "" { + if fi, err := os.Stat(v); err == nil && !fi.IsDir() { + return v + } + } + cluster := os.Getenv("E2E_CLUSTER_NAME") + if cluster == "" { + cluster = "cloudctl-e2e" + } + home, _ := os.UserHomeDir() + candidates := []string{ + filepath.Join(".", "tmp", "e2e-kubeconfig"), + filepath.Join(home, ".config", "k3d", "kubeconfig-"+cluster+".yaml"), + filepath.Join(".", "test", "e2e", "e2e-kubeconfig"), + } + for _, c := range candidates { + if fi, err := os.Stat(c); err == nil && !fi.IsDir() { + return c + } + } + t.Skipf("kubeconfig not found. Set E2E_KUBECONFIG or run 'make e2e-up' to create a kubeconfig") + return "" +} diff --git a/e2e/version_test.go b/e2e/version_test.go new file mode 100644 index 0000000..82fd5ea --- /dev/null +++ b/e2e/version_test.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build e2e +// +build e2e + +package e2e + +import ( + "encoding/json" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" +) + +func TestE2E_Version(t *testing.T) { + g := NewWithT(t) + + bin := resolveBin(t) + requireFileG(g, bin) + + stdout, stderr, err := runCmd(bin, "version", "--json") + g.Expect(err).ToNot(HaveOccurred(), "stderr: %s", stderr) + + var vi versionInfo + g.Expect(json.Unmarshal([]byte(stdout), &vi)).To(Succeed(), "output: %s", stdout) + + g.Expect(vi.Version).ToNot(BeEmpty()) + g.Expect(vi.GoVersion).ToNot(BeEmpty()) + g.Expect(vi.Platform).ToNot(BeEmpty()) + + _ = filepath.Separator // keep filepath import used on all platforms +} diff --git a/go.mod b/go.mod index 41d2bd4..6b11317 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,28 @@ module github.com/cloudoperators/cloudctl -go 1.24.0 - -toolchain go1.24.2 +go 1.25.0 require ( - github.com/cloudoperators/greenhouse v0.3.0-rc.1 + github.com/cloudoperators/greenhouse v0.5.0 + github.com/onsi/gomega v1.37.0 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 - k8s.io/apimachinery v0.33.0 - k8s.io/client-go v0.33.0 + k8s.io/apimachinery v0.33.3 + k8s.io/client-go v0.33.3 sigs.k8s.io/controller-runtime v0.20.4 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // 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 @@ -31,37 +30,37 @@ require ( 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/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sagikazarmark/locafero v0.9.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.14.0 // indirect - github.com/spf13/cast v1.8.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.12.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.0 // indirect - k8s.io/apiextensions-apiserver v0.33.0 // indirect + k8s.io/api v0.33.3 // indirect + k8s.io/apiextensions-apiserver v0.33.2 // 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-20250502105355-0f33e8f1c979 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index abc8d6b..d9447d8 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,48 @@ -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudoperators/greenhouse v0.3.0-rc.1 h1:GDBRKsp/BkEwqQQBFbCJ+xmTUxoCUt4JpDaM561mF84= -github.com/cloudoperators/greenhouse v0.3.0-rc.1/go.mod h1:NkahCYG5hNNMe6JgRscUI6KqHCUGhnc6i2+95Y5WIU0= +github.com/cloudoperators/greenhouse v0.5.0 h1:9lRxtdqH3gSlHaaYM5Irl6se18yfy16ijD3Y01MYcZw= +github.com/cloudoperators/greenhouse v0.5.0/go.mod h1:jWHv+WR1lG8DYDz8FwoIGAYFIbKJnKv6aYeeID+M1+0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dexidp/dex v0.0.0-20240807174518-43956db7fd75 h1:rhJGxE3OIwZQmReo7UO/u+Qd/DsH4pZUBaUr2wrSxp0= -github.com/dexidp/dex v0.0.0-20240807174518-43956db7fd75/go.mod h1:r4AnI+KSlse0g9cmP+swnmsJscyXlMwDOugBOt5yWcA= -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/dexidp/dex v0.0.0-20250522090151-6e602d3315ea h1:D+mPqLu8KWszDXG+qL097KgitG4mgAUxKIw/90UrIgw= +github.com/dexidp/dex v0.0.0-20250522090151-6e602d3315ea/go.mod h1:2WldReXEUzo69gL61pdGCM/i49BH2FsMQMHo6ukzEpk= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -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.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -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-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= +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/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -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/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 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/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -53,8 +53,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN 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= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -75,10 +75,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -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/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -88,12 +88,12 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.23.0 h1:FA1xjp8ieYDzlgS5ABTpdUDB7wtngggONc8a7ku2NqQ= -github.com/onsi/ginkgo/v2 v2.23.0/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -112,14 +112,14 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= -github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= -github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= -github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -140,8 +140,8 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.starlark.net v0.0.0-20240725214946-42030a7cedce h1:YyGqCjZtGZJ+mRPaenEiB87afEO2MFRzLiJNZ0Z0bPw= -go.starlark.net v0.0.0-20240725214946-42030a7cedce/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -149,23 +149,21 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -175,16 +173,16 @@ golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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= @@ -200,40 +198,38 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.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/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= -k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= -k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= -k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= -k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= -k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/cli-runtime v0.31.6 h1:Ki+WKjczcP6m0C6DuWlmkavTKiDxGDXivPqRUK2MfrY= -k8s.io/cli-runtime v0.31.6/go.mod h1:ubUsw37s/wJUqQ6p88OIBlLJZxVWnq5Ny2tDNxPtwCs= -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.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y= +k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= 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/kubectl v0.31.6 h1:ngzql/UugqpEbeeyQX678BlVHXks19JR3CFjwKnWuFI= -k8s.io/kubectl v0.31.6/go.mod h1:m6OXbx9s0sZiaZrfHHSEmJUD5CjWPA5+cVg0GZnVdzM= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y= +k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI= +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/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= -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/kustomize/api v0.17.3 h1:6GCuHSsxq7fN5yhF2XrC+AAr8gxQwhexgHflOAD/JJU= -sigs.k8s.io/kustomize/api v0.17.3/go.mod h1:TuDH4mdx7jTfK61SQ/j1QZM/QWR+5rmEiNjvYlhzFhc= -sigs.k8s.io/kustomize/kyaml v0.17.2 h1:+AzvoJUY0kq4QAhH/ydPHHMRLijtUKiyVyh7fOSshr0= -sigs.k8s.io/kustomize/kyaml v0.17.2/go.mod h1:9V0mCjIEYjlXuCdYsSXvyoy2BTsLESH7TlGV81S282U= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 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/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go index dd8f0c0..b145f3d 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,11 @@ package main import ( + "context" "fmt" "os" + "os/signal" + "syscall" "github.com/spf13/viper" @@ -19,8 +22,13 @@ func main() { viper.SetEnvPrefix("CLOUDCTL") viper.AutomaticEnv() - if err := cmd.Execute(); err != nil { - fmt.Println(err) + // Graceful cancellation on SIGINT/SIGTERM + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + if err := cmd.Execute(ctx); err != nil { + // Print errors to stderr + fmt.Fprintln(os.Stderr, err) os.Exit(1) } }