diff --git a/.github/workflows/publish-clm.yaml b/.github/workflows/publish-clm.yaml new file mode 100644 index 00000000..99c0dcce --- /dev/null +++ b/.github/workflows/publish-clm.yaml @@ -0,0 +1,54 @@ +name: Publish clm + +on: + release: + types: [published] + +defaults: + run: + shell: bash + +jobs: + deploy: + runs-on: [ubuntu-24.04] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Build + run: | + mkdir -p bin + for os in linux darwin; do + for arch in amd64 arm64; do + file=bin/clm-$os-$arch + echo "Building $file ..." + LDFLAGS="" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.version=${{ github.event.release.tag_name }}\"" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.gitCommit=${{ github.sha }}\"" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.gitTreeState=clean\"" + GOOS=$os GOARCH=$arch go build -o $file -ldflags "$LDFLAGS" ./clm + done + done + + - name: Upload + run: | + for os in linux darwin; do + for arch in amd64 arm64; do + upload_url="${{ github.event.release.upload_url }}" + upload_url=${upload_url%%\{*\}} + file=bin/clm-$os-$arch + echo "Uploading $file to $upload_url ..." + curl -sSf \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "Content-Type: $(file -b --mime-type $file)" \ + --data-binary @$file \ + "$upload_url?name=$(basename $file)" + done + done \ No newline at end of file diff --git a/.github/workflows/publish-scaffold.yaml b/.github/workflows/publish-scaffold.yaml index 74505177..0e79a51a 100644 --- a/.github/workflows/publish-scaffold.yaml +++ b/.github/workflows/publish-scaffold.yaml @@ -1,4 +1,4 @@ -name: Publish scaffolder +name: Publish scaffold on: release: @@ -29,9 +29,9 @@ jobs: file=bin/scaffold-$os-$arch echo "Building $file ..." LDFLAGS="" - LDFLAGS+=" -X github.com/sap/component-operator-runtime/internal/version.version=${{ github.event.release.tag_name }}" - LDFLAGS+=" -X github.com/sap/component-operator-runtime/internal/version.gitCommit=${{ github.sha }}" - LDFLAGS+=" -X github.com/sap/component-operator-runtime/internal/version.gitTreeState=clean" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.version=${{ github.event.release.tag_name }}\"" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.gitCommit=${{ github.sha }}\"" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.gitTreeState=clean\"" GOOS=$os GOARCH=$arch go build -o $file -ldflags "$LDFLAGS" ./scaffold done done diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 76ce5d92..b8f7b244 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -74,7 +74,11 @@ jobs: - name: Build scaffold run: | - go build -o bin/scaffold -ldflags "-X \"main.version=$(pwd)\"" ./scaffold + LDFLAGS="" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.version=$(pwd)\"" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.gitCommit=${{ github.sha }}\"" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.gitTreeState=clean\"" + go build -o bin/scaffold -ldflags "$LDFLAGS" ./scaffold echo "Running 'scaffold --version' ..." bin/scaffold --version @@ -141,3 +145,28 @@ jobs: # run: | # cd $RUNNER_TEMP/src # make docker-build + + test-clm: + runs-on: ubuntu-24.04 + needs: + - check-generate + - unit-and-integration-tests + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Build clm + run: | + LDFLAGS="" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.version=$(pwd)\"" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.gitCommit=${{ github.sha }}\"" + LDFLAGS+=" -X \"github.com/sap/component-operator-runtime/internal/version.gitTreeState=clean\"" + go build -o bin/clm -ldflags "$LDFLAGS" ./clm + echo "Running 'clm version -o json' ..." + bin/clm version -o json \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1b23deb3..19e7e8d3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ __debug_bin *.swo *~ .DS_Store +.vscode # temp stuff /tmp diff --git a/clm/clm.go b/clm/clm.go new file mode 100644 index 00000000..ceebbda9 --- /dev/null +++ b/clm/clm.go @@ -0,0 +1,26 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "os" + + "github.com/go-logr/logr" + + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/sap/component-operator-runtime/clm/cmd" +) + +func main() { + // ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + + log.SetLogger(logr.Discard()) + + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/clm/cmd/apply.go b/clm/cmd/apply.go new file mode 100644 index 00000000..d43f6a88 --- /dev/null +++ b/clm/cmd/apply.go @@ -0,0 +1,151 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cmd + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + "github.com/sap/component-operator-runtime/clm/internal/backoff" + "github.com/sap/component-operator-runtime/clm/internal/manifests" + "github.com/sap/component-operator-runtime/clm/internal/release" + "github.com/sap/component-operator-runtime/pkg/component" + "github.com/sap/component-operator-runtime/pkg/reconciler" + "github.com/sap/go-generics/slices" +) + +const applyUsage = `Apply component manifests to Kubernetes cluster` + +type applyOptions struct { + valuesSources []string + timeout time.Duration +} + +func newApplyCmd() *cobra.Command { + options := &applyOptions{} + + cmd := &cobra.Command{ + Use: "apply NAME SOURCE...", + Short: "Apply component", + Long: applyUsage, + SilenceUsage: true, + Args: cobra.MinimumNArgs(2), + PreRunE: func(c *cobra.Command, args []string) error { + return nil + }, + RunE: func(c *cobra.Command, args []string) (err error) { + name := args[0] + manifestSources := args[1:] + namespace := c.Flag("namespace").Value.String() + + clnt, err := getClient(c.Flag("kubeconfig").Value.String()) + if err != nil { + return err + } + + reconciler := reconciler.NewReconciler(fullName, clnt, reconciler.ReconcilerOptions{ + UpdatePolicy: ref(reconciler.UpdatePolicySsaOverride), + }) + + releaseClient := release.NewClient(fullName, clnt) + + ownerId := fullName + "/" + namespace + "/" + name + + objects, err := manifests.Generate(manifestSources, options.valuesSources, fullName, clnt, namespace, name) + if err != nil { + return err + } + + release, err := releaseClient.Get(context.TODO(), namespace, name) + if err != nil { + if apierrors.IsNotFound(err) { + release, err = releaseClient.Create(context.TODO(), namespace, name) + if err != nil { + return err + } + } else { + return err + } + } + + if release.IsDeleting() { + return fmt.Errorf("release %s/%s is being deleted; updates are not allowed in this state", release.GetNamespace(), release.GetName()) + } + + release.Revision += 1 + + backoff := backoff.New() + + var timeout <-chan time.Time + if options.timeout > 0 { + timeout = time.After(options.timeout) + } + + defer func() { + if err != nil { + release.State = component.StateError + } + if updateErr := releaseClient.Update(context.TODO(), release); updateErr != nil { + err = utilerrors.NewAggregate([]error{err, updateErr}) + } + }() + + for { + release.State = component.StateProcessing + ok, err := reconciler.Apply(context.TODO(), &release.Inventory, objects, namespace, ownerId, release.Revision) + if err != nil { + return err + } + if ok { + release.State = component.StateReady + break + } + if err := releaseClient.Update(context.TODO(), release); err != nil { + return err + } + select { + case <-time.After(backoff.Next()): + case <-timeout: + return fmt.Errorf("timeout applying release %s/%s", release.GetNamespace(), release.GetName()) + } + } + + fmt.Printf("Release %s/%s successfully applied\n", release.GetNamespace(), release.GetName()) + + return nil + }, + ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveDefault + } + if clnt, err := getClient(c.Flag("kubeconfig").Value.String()); err == nil { + releaseClient := release.NewClient(fullName, clnt) + namespace := c.Flag("namespace").Value.String() + if namespace == "" { + namespace = "default" + } + ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second) + defer cancel() + if releases, err := releaseClient.List(ctx, namespace); err == nil { + return slices.Collect(releases, func(release *release.Release) string { return release.GetName() }), cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveDefault + }, + } + + flags := cmd.Flags() + flags.StringArrayVarP(&options.valuesSources, "values", "f", nil, "Path to values file in yaml format (can be repeated, values will be merged in order of appearance)") + flags.DurationVar(&options.timeout, "timeout", 0, "Time to wait for the operation to complete (default is to wait forever)") + + return cmd +} diff --git a/clm/cmd/delete.go b/clm/cmd/delete.go new file mode 100644 index 00000000..95454c6b --- /dev/null +++ b/clm/cmd/delete.go @@ -0,0 +1,138 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cmd + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + "github.com/sap/component-operator-runtime/clm/internal/backoff" + "github.com/sap/component-operator-runtime/clm/internal/release" + "github.com/sap/component-operator-runtime/pkg/component" + "github.com/sap/component-operator-runtime/pkg/reconciler" + "github.com/sap/go-generics/slices" +) + +const deleteUsage = `Delete component from Kubernetes cluster` + +type deleteOptions struct { + timeout time.Duration +} + +func newDeleteCmd() *cobra.Command { + options := &deleteOptions{} + + cmd := &cobra.Command{ + Use: "delete NAME", + Short: "Delete component", + Long: deleteUsage, + SilenceUsage: true, + Args: cobra.ExactArgs(1), + PreRunE: func(c *cobra.Command, args []string) error { + return nil + }, + RunE: func(c *cobra.Command, args []string) (err error) { + name := args[0] + namespace := c.Flag("namespace").Value.String() + + clnt, err := getClient(c.Flag("kubeconfig").Value.String()) + if err != nil { + return err + } + + reconciler := reconciler.NewReconciler(fullName, clnt, reconciler.ReconcilerOptions{ + UpdatePolicy: ref(reconciler.UpdatePolicySsaOverride), + }) + + releaseClient := release.NewClient(fullName, clnt) + + release, err := releaseClient.Get(context.TODO(), namespace, name) + if err != nil { + return err + } + + if ok, msg, err := reconciler.IsDeletionAllowed(context.TODO(), &release.Inventory); err != nil { + return err + } else if !ok { + return fmt.Errorf(msg) + } + + if err := releaseClient.Delete(context.TODO(), release); err != nil { + return err + } + release, err = releaseClient.Get(context.TODO(), namespace, name) + if err != nil { + return err + } + + backoff := backoff.New() + + var timeout <-chan time.Time + if options.timeout > 0 { + timeout = time.After(options.timeout) + } + + defer func() { + if err != nil { + release.State = component.StateError + } + if updateErr := releaseClient.Update(context.TODO(), release); updateErr != nil { + err = utilerrors.NewAggregate([]error{err, updateErr}) + } + }() + + for { + release.State = component.StateDeleting + ok, err := reconciler.Delete(context.TODO(), &release.Inventory) + if err != nil { + return err + } + if ok { + break + } + if err := releaseClient.Update(context.TODO(), release); err != nil { + return err + } + select { + case <-time.After(backoff.Next()): + case <-timeout: + return fmt.Errorf("timeout deleting release %s/%s", release.GetNamespace(), release.GetName()) + } + } + + fmt.Printf("Release %s/%s successfully deleted\n", release.GetNamespace(), release.GetName()) + + return nil + }, + ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + if clnt, err := getClient(c.Flag("kubeconfig").Value.String()); err == nil { + releaseClient := release.NewClient(fullName, clnt) + namespace := c.Flag("namespace").Value.String() + if namespace == "" { + namespace = "default" + } + ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second) + defer cancel() + if releases, err := releaseClient.List(ctx, namespace); err == nil { + return slices.Collect(releases, func(release *release.Release) string { return release.GetName() }), cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveDefault + }, + } + + flags := cmd.Flags() + flags.DurationVar(&options.timeout, "timeout", 0, "Time to wait for the operation to complete (default is to wait forever)") + + return cmd +} diff --git a/clm/cmd/list.go b/clm/cmd/list.go new file mode 100644 index 00000000..1bff6ecf --- /dev/null +++ b/clm/cmd/list.go @@ -0,0 +1,106 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + "github.com/sap/go-generics/slices" + "github.com/spf13/cobra" + + kyaml "sigs.k8s.io/yaml" + + "github.com/sap/component-operator-runtime/clm/internal/release" +) + +const listUsage = `List components` + +type listOptions struct { + allNamespaces bool + outputFormat string +} + +func newListCmd() *cobra.Command { + options := &listOptions{} + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List components", + Long: listUsage, + SilenceUsage: true, + Args: cobra.NoArgs, + PreRunE: func(c *cobra.Command, args []string) error { + switch options.outputFormat { + case "table", "yaml", "yamlstream", "json": + return nil + default: + return fmt.Errorf("invalid value for flag --%s: %s", "output", options.outputFormat) + } + }, + RunE: func(c *cobra.Command, args []string) error { + namespace := c.Flag("namespace").Value.String() + if options.allNamespaces { + namespace = "" + } + + clnt, err := getClient(c.Flag("kubeconfig").Value.String()) + if err != nil { + return err + } + + releaseClient := release.NewClient(fullName, clnt) + + releases, err := releaseClient.List(context.TODO(), namespace) + if err != nil { + return err + } + + switch options.outputFormat { + case "table": + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", "NAMESPACE", "NAME", "REVISION", "STATE", "OBJECTS", "READY", "COMPLETED", "CREATED", "UPDATED") + for _, release := range releases { + details := getReleaseDetails(release) + fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%d\t%d\t%d\t%s\t%s\t\n", + details.Namespace, + details.Name, + details.Revision, + details.State, + details.NumAllObjects, + details.NumReadyObjects, + details.NumCompletedObjects, + details.CreatedAt, + details.LastUpdatedAt, + ) + } + w.Flush() + case "yaml": + fmt.Printf("%s", string(must(kyaml.Marshal(slices.Collect(releases, getReleaseDetails))))) + case "yamlstream": + for _, release := range releases { + fmt.Printf("---\n%s", must(kyaml.Marshal(getReleaseDetails(release)))) + } + case "json": + fmt.Printf("%s\n", string(must(json.MarshalIndent(slices.Collect(releases, getReleaseDetails), "", " ")))) + } + return nil + }, + ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&options.allNamespaces, "all-namespaces", "A", false, "List components across all namespaces") + flags.StringVarP(&options.outputFormat, "output", "o", "table", "Output format; one of \"table\", \"yaml\", \"yamlstream\" or \"json\"") + + return cmd +} diff --git a/clm/cmd/root.go b/clm/cmd/root.go new file mode 100644 index 00000000..5495d664 --- /dev/null +++ b/clm/cmd/root.go @@ -0,0 +1,74 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cmd + +import ( + "context" + "time" + + "github.com/sap/go-generics/slices" + "github.com/spf13/cobra" + + corev1 "k8s.io/api/core/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +const ( + fullName = "clm.cs.sap.com" + shortName = "clm" +) + +const rootUsage = `A Kubernetes package manager + +Common actions for clm: +- clm apply Apply given component manifests to Kubernetes cluster +- clm delete Remove component from Kubernetes cluster +- clm status Show component status +- clm ls List components +` + +func newRootCmd() *cobra.Command { + configFlags := genericclioptions.NewConfigFlags(true) + configFlags.Namespace = ref("default") + + cmd := &cobra.Command{ + Use: shortName, + Short: "A Kubernetes component manager", + Long: rootUsage, + SilenceUsage: true, + } + + cmd.Flags().SortFlags = false + configFlags.AddFlags(cmd.PersistentFlags()) + + if err := cmd.RegisterFlagCompletionFunc("namespace", func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if clnt, err := getClient(c.Flag("kubeconfig").Value.String()); err == nil { + namespaceList := &corev1.NamespaceList{} + ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second) + defer cancel() + if err := clnt.List(ctx, namespaceList); err == nil { + return slices.Collect(namespaceList.Items, func(namespace corev1.Namespace) string { return namespace.Name }), cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveDefault + }); err != nil { + panic(err) + } + + cmd.AddCommand( + newVersionCmd(), + newApplyCmd(), + newDeleteCmd(), + newStatusCmd(), + newListCmd(), + ) + + return cmd +} + +func Execute() error { + return newRootCmd().Execute() +} diff --git a/clm/cmd/status.go b/clm/cmd/status.go new file mode 100644 index 00000000..90a63852 --- /dev/null +++ b/clm/cmd/status.go @@ -0,0 +1,109 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "text/tabwriter" + "time" + + "github.com/sap/go-generics/slices" + "github.com/spf13/cobra" + + kyaml "sigs.k8s.io/yaml" + + "github.com/sap/component-operator-runtime/clm/internal/release" +) + +const statusUsage = `Show component status` + +type statusOptions struct { + outputFormat string +} + +func newStatusCmd() *cobra.Command { + options := &statusOptions{} + + cmd := &cobra.Command{ + Use: "status NAME", + Short: "Show component status", + Long: statusUsage, + SilenceUsage: true, + Args: cobra.ExactArgs(1), + PreRunE: func(c *cobra.Command, args []string) error { + switch options.outputFormat { + case "table", "yaml", "json": + return nil + default: + return fmt.Errorf("invalid value for flag --%s: %s", "output", options.outputFormat) + } + }, + RunE: func(c *cobra.Command, args []string) error { + name := args[0] + namespace := c.Flag("namespace").Value.String() + + clnt, err := getClient(c.Flag("kubeconfig").Value.String()) + if err != nil { + return err + } + + releaseClient := release.NewClient(fullName, clnt) + + release, err := releaseClient.Get(context.TODO(), namespace, name) + if err != nil { + return err + } + + switch options.outputFormat { + case "table": + details := getReleaseDetails(release) + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + fmt.Fprintf(w, "%s:\t%s\t\n", "Namespace", details.Namespace) + fmt.Fprintf(w, "%s:\t%s\t\n", "Name", details.Name) + fmt.Fprintf(w, "%s:\t%d\t\n", "Revision", details.Revision) + fmt.Fprintf(w, "%s:\t%s\t\n", "State", details.State) + fmt.Fprintf(w, "%s:\t%d\t\n", "Number of objects", details.NumAllObjects) + fmt.Fprintf(w, "%s:\t%d\t\n", "Number of ready objects", details.NumReadyObjects) + fmt.Fprintf(w, "%s:\t%d\t\n", "Number of completed objects", details.NumCompletedObjects) + fmt.Fprintf(w, "%s:\t%s\t\n", "Created at", details.CreatedAt) + fmt.Fprintf(w, "%s:\t%s\t\n", "Last updated at", details.LastUpdatedAt) + w.Flush() + case "yaml": + fmt.Printf("%s", string(must(kyaml.Marshal(getReleaseDetails(release))))) + case "json": + fmt.Printf("%s\n", string(must(json.MarshalIndent(getReleaseDetails(release), "", " ")))) + } + + return nil + }, + ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + if clnt, err := getClient(c.Flag("kubeconfig").Value.String()); err == nil { + releaseClient := release.NewClient(fullName, clnt) + namespace := c.Flag("namespace").Value.String() + if namespace == "" { + namespace = "default" + } + ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second) + defer cancel() + if releases, err := releaseClient.List(ctx, namespace); err == nil { + return slices.Collect(releases, func(release *release.Release) string { return release.GetName() }), cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveDefault + }, + } + + flags := cmd.Flags() + flags.StringVarP(&options.outputFormat, "output", "o", "table", "Output format; one of \"table\", \"yaml\" or \"json\"") + + return cmd +} diff --git a/clm/cmd/util.go b/clm/cmd/util.go new file mode 100644 index 00000000..228cc0f9 --- /dev/null +++ b/clm/cmd/util.go @@ -0,0 +1,98 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/sap/go-generics/slices" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" + + "github.com/sap/component-operator-runtime/clm/internal/release" + "github.com/sap/component-operator-runtime/internal/cluster" + "github.com/sap/component-operator-runtime/pkg/reconciler" +) + +// TODO: consolidate all the util files into an internal reuse package + +func ref[T any](x T) *T { + return &x +} + +func must[T any](x T, err error) T { + if err != nil { + panic(err) + } + return x +} + +func getClient(kubeConfigPath string) (cluster.Client, error) { + if kubeConfigPath == "" { + kubeConfigPath = os.Getenv("KUBECONFIG") + } + if kubeConfigPath == "" { + return nil, fmt.Errorf("no kubeconfig was specified") + } + kubeConfig, err := os.ReadFile(kubeConfigPath) + if err != nil { + return nil, err + } + config, err := clientcmd.RESTConfigFromKubeConfig(kubeConfig) + if err != nil { + return nil, err + } + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) + utilruntime.Must(apiregistrationv1.AddToScheme(scheme)) + return cluster.NewClientFor(config, scheme, fullName) +} + +func formatTimestamp(t time.Time) string { + d := time.Now().Sub(t) + if d > 24*time.Hour { + return fmt.Sprintf("%dd", d/24*time.Hour) + } else if d > time.Hour { + return fmt.Sprintf("%dh", d/time.Hour) + } else if d > time.Minute { + return fmt.Sprintf("%dm", d/time.Minute) + } else { + return fmt.Sprintf("%ds", d/time.Second) + } +} + +type releaseDetails struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Revision int64 `json:"revision"` + State string `json:"state"` + NumAllObjects int64 `json:"numAllObjects"` + NumReadyObjects int64 `json:"numReadyObjects"` + NumCompletedObjects int64 `json:"numCompletedObjects"` + CreatedAt string `json:"createdAt"` + LastUpdatedAt string `json:"lastUpdatedAt"` +} + +func getReleaseDetails(release *release.Release) *releaseDetails { + return &releaseDetails{ + Namespace: release.GetNamespace(), + Name: release.GetName(), + Revision: release.Revision, + State: string(release.State), + NumAllObjects: int64(len(release.Inventory)), + NumReadyObjects: int64(slices.Count(release.Inventory, func(item *reconciler.InventoryItem) bool { return item.Phase == reconciler.PhaseReady })), + NumCompletedObjects: int64(slices.Count(release.Inventory, func(item *reconciler.InventoryItem) bool { return item.Phase == reconciler.PhaseCompleted })), + CreatedAt: formatTimestamp(*release.GetCreationTimestamp()), + LastUpdatedAt: formatTimestamp(*release.GetUpdateTimestamp()), + } +} diff --git a/clm/cmd/version.go b/clm/cmd/version.go new file mode 100644 index 00000000..36d27e70 --- /dev/null +++ b/clm/cmd/version.go @@ -0,0 +1,64 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + + kyaml "sigs.k8s.io/yaml" + + "github.com/sap/component-operator-runtime/internal/version" +) + +const versionUsage = `Show component lifecycle manager (clm) version` + +type versionOptions struct { + outputFormat string +} + +func newVersionCmd() *cobra.Command { + options := &versionOptions{} + + cmd := &cobra.Command{ + Use: "version", + Short: "Show version", + Long: versionUsage, + SilenceUsage: true, + Args: cobra.NoArgs, + PreRunE: func(c *cobra.Command, args []string) error { + switch options.outputFormat { + case "short", "yaml", "json": + return nil + default: + return fmt.Errorf("invalid value for flag --%s: %s", "output", options.outputFormat) + } + }, + Run: func(c *cobra.Command, args []string) { + buildInfo := version.GetBuildInfo() + switch options.outputFormat { + case "short": + fmt.Printf("%s\n", buildInfo.Version) + case "yaml": + fmt.Printf("%s", string(must(kyaml.Marshal(buildInfo)))) + case "json": + fmt.Printf("%s\n", string(must(json.MarshalIndent(buildInfo, "", " ")))) + default: + panic("this cannot happen") + } + }, + ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp + }, + } + + flags := cmd.Flags() + flags.StringVarP(&options.outputFormat, "output", "o", "short", "Output format; one of \"short\", \"yaml\" or \"json\"") + + return cmd +} diff --git a/clm/internal/backoff/backoff.go b/clm/internal/backoff/backoff.go new file mode 100644 index 00000000..5d8779ff --- /dev/null +++ b/clm/internal/backoff/backoff.go @@ -0,0 +1,33 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package backoff + +import "time" + +const ( + minBackoff = time.Millisecond + maxBackoff = 10 * time.Second +) + +type Backoff struct { + duration time.Duration +} + +func New() *Backoff { + return &Backoff{} +} + +func (b *Backoff) Next() time.Duration { + if b.duration < minBackoff { + b.duration = minBackoff + } else { + b.duration *= 2 + } + if b.duration > maxBackoff { + b.duration = maxBackoff + } + return b.duration +} diff --git a/clm/internal/manifests/generate.go b/clm/internal/manifests/generate.go new file mode 100644 index 00000000..c6e96f73 --- /dev/null +++ b/clm/internal/manifests/generate.go @@ -0,0 +1,96 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package manifests + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "sigs.k8s.io/controller-runtime/pkg/client" + kyaml "sigs.k8s.io/yaml" + + "github.com/sap/component-operator-runtime/pkg/cluster" + "github.com/sap/component-operator-runtime/pkg/component" + "github.com/sap/component-operator-runtime/pkg/manifests" + "github.com/sap/component-operator-runtime/pkg/manifests/helm" + "github.com/sap/component-operator-runtime/pkg/manifests/kustomize" + "github.com/sap/component-operator-runtime/pkg/types" +) + +func Generate(manifestSources []string, valuesSources []string, reconcilerName string, clnt cluster.Client, namespace string, name string) ([]client.Object, error) { + var allObjects []client.Object + var allValues = make(map[string]any) + + for _, source := range valuesSources { + // TODO: suuport URLs + path := source + + rawValues, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var values map[string]any + if err := kyaml.Unmarshal(rawValues, &values); err != nil { + return nil, err + } + manifests.MergeMapInto(allValues, values) + } + + for _, source := range manifestSources { + // TODO: support helm, oci URLs + path := source + + if info, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("no such file or directory: %s", path) + } else { + return nil, err + } + } else if !info.IsDir() { + return nil, fmt.Errorf("not a directory: %s", path) + } + path, err := filepath.Abs(source) + if err != nil { + return nil, err + } + fsys := os.DirFS(path) + + var generator manifests.Generator + if _, err = fs.Stat(fsys, "Chart.yaml"); err == nil { + generator, err = helm.NewHelmGenerator(fsys, "", clnt) + if err != nil { + return nil, err + } + } else if errors.Is(err, fs.ErrNotExist) { + generator, err = kustomize.NewKustomizeGenerator(fsys, "", clnt, kustomize.KustomizeGeneratorOptions{}) + if err != nil { + return nil, err + } + } else { + return nil, err + } + + // TODO: what about component and component digest + generateCtx := component.NewContext(context.TODO()). + WithReconcilerName(reconcilerName). + WithClient(clnt). + WithComponent(nil). + WithComponentDigest("") + objects, err := generator.Generate(generateCtx, namespace, name, types.UnstructurableMap(allValues)) + if err != nil { + return nil, err + } + + allObjects = append(allObjects, objects...) + } + + return allObjects, nil +} diff --git a/clm/internal/release/client.go b/clm/internal/release/client.go new file mode 100644 index 00000000..706a06c4 --- /dev/null +++ b/clm/internal/release/client.go @@ -0,0 +1,132 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package release + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/sap/go-generics/slices" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apitypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + labelKeyRelease = "release.clm.cs.sap.com" +) + +type Client struct { + name string + finalizer string + client client.Client +} + +func NewClient(name string, clnt client.Client) *Client { + return &Client{ + name: name, + finalizer: fmt.Sprintf("%s/finalizer", name), + client: clnt, + } +} + +func (c *Client) Get(ctx context.Context, namespace string, name string) (*Release, error) { + release := &Release{ + namespace: namespace, + name: name, + configMap: &corev1.ConfigMap{}, + } + if err := c.client.Get(ctx, apitypes.NamespacedName{Namespace: namespace, Name: c.configMapName(name)}, release.configMap); err != nil { + return nil, err + } + if err := release.importData(); err != nil { + return nil, err + } + return release, nil +} + +func (c *Client) List(ctx context.Context, namespace string) ([]*Release, error) { + configMapList := &corev1.ConfigMapList{} + if err := c.client.List(ctx, configMapList, client.InNamespace(namespace), client.HasLabels{labelKeyRelease}); err != nil { + return nil, err + } + releases := make([]*Release, len(configMapList.Items)) + for i := 0; i < len(configMapList.Items); i++ { + configMap := &configMapList.Items[i] + releases[i] = &Release{ + namespace: configMap.Namespace, + name: c.releaseName(configMap.Name), + configMap: configMap, + } + if err := releases[i].importData(); err != nil { + return nil, err + } + } + return releases, nil +} + +func (c *Client) Create(ctx context.Context, namespace string, name string) (*Release, error) { + now := time.Now() + + release := &Release{ + namespace: namespace, + name: name, + creationTimestamp: &now, + updateTimestamp: &now, + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: c.configMapName(name), + Labels: map[string]string{ + labelKeyRelease: name, + }, + Finalizers: []string{c.finalizer}, + }, + }, + } + if err := release.exportData(); err != nil { + return nil, err + } + if err := c.client.Create(ctx, release.configMap); err != nil { + return nil, err + } + return release, nil +} + +func (c *Client) Update(ctx context.Context, release *Release) error { + if release.configMap.UID == "" || release.configMap.ResourceVersion == "" { + return fmt.Errorf("error updating release %s/%s: empty uid or resource version", release.GetNamespace(), release.GetName()) + } + if !release.configMap.DeletionTimestamp.IsZero() && len(release.Inventory) == 0 { + controllerutil.RemoveFinalizer(release.configMap, c.finalizer) + } + now := time.Now() + release.updateTimestamp = &now + if err := release.exportData(); err != nil { + return err + } + return c.client.Update(ctx, release.configMap) +} + +func (c *Client) Delete(ctx context.Context, release *Release) error { + if release.configMap.UID == "" || release.configMap.ResourceVersion == "" { + return fmt.Errorf("error deleting release %s/%s: empty uid or resource version", release.GetNamespace(), release.GetName()) + } + return c.client.Delete(ctx, release.configMap, client.Preconditions{ResourceVersion: &release.configMap.ResourceVersion}) +} + +func (c *Client) configMapName(releaseName string) string { + return fmt.Sprintf("%s.release.%s", strings.Join(slices.Reverse(strings.Split(c.name, ".")), "."), releaseName) +} + +func (c *Client) releaseName(configMapName string) string { + return configMapName[strings.LastIndex(configMapName, ".")+1:] +} diff --git a/clm/internal/release/release.go b/clm/internal/release/release.go new file mode 100644 index 00000000..42d241be --- /dev/null +++ b/clm/internal/release/release.go @@ -0,0 +1,145 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package release + +import ( + "strconv" + "time" + + corev1 "k8s.io/api/core/v1" + kyaml "sigs.k8s.io/yaml" + + "github.com/sap/component-operator-runtime/pkg/component" + "github.com/sap/component-operator-runtime/pkg/reconciler" +) + +const ( + dataKeyVersion = "version" + dataKeyCreationTimestamp = "creationTimestamp" + dataKeyUpdateTimestamp = "updateTimestamp" + dataKeyRevision = "revision" + dataKeyInventory = "inventory" + dataKeyState = "state" +) + +type Release struct { + namespace string + name string + creationTimestamp *time.Time + updateTimestamp *time.Time + configMap *corev1.ConfigMap + Revision int64 + Inventory []*reconciler.InventoryItem + State component.State +} + +func (r *Release) GetNamespace() string { + return r.namespace +} + +func (r *Release) GetName() string { + return r.name +} + +func (r *Release) IsDeleting() bool { + return !r.configMap.DeletionTimestamp.IsZero() +} + +func (r *Release) GetCreationTimestamp() *time.Time { + return r.creationTimestamp +} + +func (r *Release) GetUpdateTimestamp() *time.Time { + return r.updateTimestamp +} + +func (r *Release) importData() error { + if creationTimestampData, ok := r.configMap.Data[dataKeyCreationTimestamp]; ok { + creationTimestamp, err := time.Parse(time.RFC3339, creationTimestampData) + if err != nil { + return err + } + r.creationTimestamp = &creationTimestamp + } else { + r.creationTimestamp = nil + } + + if updateTimestampData, ok := r.configMap.Data[dataKeyUpdateTimestamp]; ok { + updateTimestamp, err := time.Parse(time.RFC3339, updateTimestampData) + if err != nil { + return err + } + r.updateTimestamp = &updateTimestamp + } else { + r.updateTimestamp = nil + } + + if revisionData, ok := r.configMap.Data[dataKeyRevision]; ok { + revision, err := strconv.ParseInt(revisionData, 10, 64) + if err != nil { + return err + } + r.Revision = revision + } else { + r.Revision = 0 + } + + if inventoryData, ok := r.configMap.Data[dataKeyInventory]; ok { + if err := kyaml.Unmarshal([]byte(inventoryData), &r.Inventory); err != nil { + return err + } + } else { + r.Inventory = nil + } + + if stateData, ok := r.configMap.Data[dataKeyState]; ok { + r.State = component.State(stateData) + } else { + r.State = "" + } + + return nil +} + +func (r *Release) exportData() error { + if r.configMap.Data == nil { + r.configMap.Data = make(map[string]string) + } + + r.configMap.Data[dataKeyVersion] = "1" + + if r.creationTimestamp != nil { + r.configMap.Data[dataKeyCreationTimestamp] = r.creationTimestamp.Format(time.RFC3339) + } else { + delete(r.configMap.Data, dataKeyCreationTimestamp) + } + + if r.updateTimestamp != nil { + r.configMap.Data[dataKeyUpdateTimestamp] = r.updateTimestamp.Format(time.RFC3339) + } else { + delete(r.configMap.Data, dataKeyUpdateTimestamp) + } + + r.configMap.Data[dataKeyRevision] = strconv.FormatInt(r.Revision, 10) + + if len(r.Inventory) > 0 { + inventoryRawData, err := kyaml.Marshal(r.Inventory) + if err != nil { + return err + } + r.configMap.Data[dataKeyInventory] = string(inventoryRawData) + } else { + delete(r.configMap.Data, dataKeyInventory) + } + + if r.State != "" { + r.configMap.Data[dataKeyState] = string(r.State) + } else { + delete(r.configMap.Data, dataKeyState) + } + + return nil +} diff --git a/go.mod b/go.mod index 3a76d3c5..ef8e31a4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.2 require ( github.com/Masterminds/sprig/v3 v3.3.0 + github.com/go-logr/logr v1.4.2 github.com/hashicorp/go-multierror v1.1.1 github.com/iancoleman/strcase v0.3.0 github.com/onsi/ginkgo/v2 v2.20.2 @@ -11,11 +12,13 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.20.4 github.com/sap/go-generics v0.2.20 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 golang.org/x/time v0.6.0 k8s.io/api v0.31.1 k8s.io/apiextensions-apiserver v0.31.1 k8s.io/apimachinery v0.31.1 + k8s.io/cli-runtime v0.30.1 k8s.io/client-go v0.31.1 k8s.io/kube-aggregator v0.31.1 sigs.k8s.io/cli-utils v0.37.2 @@ -28,6 +31,7 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -40,7 +44,6 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-errors/errors v1.5.1 // indirect - github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/swag v0.22.9 // indirect @@ -48,25 +51,31 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.0.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/imdario/mergo v0.3.16 // 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.17.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -79,6 +88,7 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/go.sum b/go.sum index 14f3eced..5d20b038 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= @@ -12,6 +14,9 @@ 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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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= @@ -48,6 +53,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -62,6 +69,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -73,6 +82,8 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -87,12 +98,16 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 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= @@ -106,6 +121,8 @@ github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4 github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +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= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -121,8 +138,7 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/sap/go-generics v0.2.19 h1:7jhLhM0Bnq8Rwl5aOIHOBJem1WJ/oLTugz9H2+HGjwA= -github.com/sap/go-generics v0.2.19/go.mod h1:GLFl9wBPK5ucywnbhkoH/tCSQy+T3cc+KJtNlzt370M= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sap/go-generics v0.2.20 h1:xpED66QCjITv2TjkduQY5wH9ogzeQr9K8lcMzQ0CPLg= github.com/sap/go-generics v0.2.20/go.mod h1:RctuDlZAmijeTY6LDaqhLYeepxdiR8AxPxlu7UQHNz0= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= @@ -131,6 +147,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -174,9 +192,12 @@ golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht 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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= @@ -220,6 +241,8 @@ k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/ k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/cli-runtime v0.30.1 h1:kSBBpfrJGS6lllc24KeniI9JN7ckOOJKnmFYH1RpTOw= +k8s.io/cli-runtime v0.30.1/go.mod h1:zhHgbqI4J00pxb6gM3gJPVf2ysDjhQmQtnTxnMScab8= k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=