From 12fa3cc46a8f8a7b04b1dbb17f16c7c699a3d50c Mon Sep 17 00:00:00 2001 From: Walter Fender Date: Tue, 26 Nov 2024 18:55:54 +0000 Subject: [PATCH] Create a kompanion command to migrate KCC. New mgirator command. Creates a migration file of KCC resources. Allows the resources to be easily copied to another cluster. Factored in changes suggested by Justin. --- experiments/kompanion/Makefile | 32 ++ experiments/kompanion/README.md | 5 +- experiments/kompanion/cmd/export/export.go | 85 +--- experiments/kompanion/cmd/export/options.go | 59 +++ .../kompanion/cmd/migrator/migrator.go | 369 ++++++++++++++++++ experiments/kompanion/cmd/migrator/options.go | 71 ++++ experiments/kompanion/cmd/summary/options.go | 64 +-- experiments/kompanion/cmd/summary/summary.go | 27 +- experiments/kompanion/main.go | 2 + experiments/kompanion/pkg/utils/options.go | 90 +++++ experiments/kompanion/pkg/utils/utils.go | 18 +- 11 files changed, 671 insertions(+), 151 deletions(-) create mode 100644 experiments/kompanion/Makefile create mode 100644 experiments/kompanion/cmd/export/options.go create mode 100644 experiments/kompanion/cmd/migrator/migrator.go create mode 100644 experiments/kompanion/cmd/migrator/options.go create mode 100644 experiments/kompanion/pkg/utils/options.go diff --git a/experiments/kompanion/Makefile b/experiments/kompanion/Makefile new file mode 100644 index 0000000000..176cfc6be2 --- /dev/null +++ b/experiments/kompanion/Makefile @@ -0,0 +1,32 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## --- +## Basic build +## --- +.PHONY: build +build: bin + GOWORK=off go build -o bin/kompanion + +bin: + mkdir bin + +## --- +## Clean build and related artifacts to make the repo 'clean' +## --- +.PHONY: clean +clean: + rm -rf bin + rm -f migration.yaml + diff --git a/experiments/kompanion/README.md b/experiments/kompanion/README.md index 9929c3e1b3..880d9c48a2 100644 --- a/experiments/kompanion/README.md +++ b/experiments/kompanion/README.md @@ -8,7 +8,8 @@ Experimental KCC companion tool to help troubleshoot, analyze and gather data ab ``` # Assumes pwd is /experiments/kompanion -$ GOWORK=off go build -o kompanion +$ mkdir bin +$ GOWORK=off go build -o bin/kompanion ``` ## Export @@ -33,4 +34,4 @@ The command will generate a timestamped report `tar.gz` file to use as a snapsho # Light Roadmap -* [ ] Debug/ audit logs for the tool itself \ No newline at end of file +* [ ] Debug/ audit logs for the tool itself diff --git a/experiments/kompanion/cmd/export/export.go b/experiments/kompanion/cmd/export/export.go index a6d3400ca1..3fd1c13032 100644 --- a/experiments/kompanion/cmd/export/export.go +++ b/experiments/kompanion/cmd/export/export.go @@ -36,8 +36,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" "sigs.k8s.io/yaml" ) @@ -59,59 +57,23 @@ const ( ) func BuildExportCmd() *cobra.Command { - var opts ExportOptions - + opts := NewExportOptions() cmd := &cobra.Command{ Use: "export", Short: "export Config Connector resources", Example: examples, RunE: func(cmd *cobra.Command, args []string) error { - return RunExport(cmd.Context(), &opts) + return RunExport(cmd.Context(), opts) }, Args: cobra.ExactArgs(0), } - cmd.Flags().StringVarP(&opts.kubeconfig, kubeconfigFlag, "", opts.kubeconfig, "path to the kubeconfig file.") - cmd.Flags().StringVarP(&opts.reportNamePrefix, reportNamePrefixFlag, "", "report", "Prefix for the report name. The tool appends a timestamp to this in the format \"YYYYMMDD-HHMMSS.milliseconds\".") - - cmd.Flags().StringArrayVarP(&opts.targetNamespaces, targetNamespacesFlag, "", []string{}, "namespace prefix to target the export tool. Targets all if empty. Can be specified multiple times.") - cmd.Flags().StringArrayVarP(&opts.ignoreNamespaces, ignoreNamespacesFlag, "", []string{"kube"}, "namespace prefix to ignore. Excludes nothing if empty. Can be specified multiple times. Defaults to \"kube\".") - - cmd.Flags().StringArrayVarP(&opts.targetObjects, targetObjectsFlag, "", []string{}, "object name prefix to target. Targets all if empty. Can be specified multiple times.") - cmd.Flags().StringArrayVarP(&opts.ignoreObjects, ignoreObjectsFlag, "", []string{}, "object name prefix to ignore. Excludes nothing if empty. Can be specified multiple times.") - - cmd.Flags().IntVarP(&opts.workerRountines, workerRoutinesFlag, "", 10, "Configure the number of worker routines to export namespaces with. Defaults to 10. ") + flags := cmd.Flags() + opts.AddFlags(flags) return cmd } -const ( - // flag names. - kubeconfigFlag = "kubeconfig" - reportNamePrefixFlag = "report-prefix" - - targetNamespacesFlag = "target-namespaces" - ignoreNamespacesFlag = "exclude-namespaces" - - targetObjectsFlag = "target-objects" - ignoreObjectsFlag = "exclude-objects" - - workerRoutinesFlag = "worker-routines" -) - -type ExportOptions struct { - kubeconfig string - reportNamePrefix string - - targetNamespaces []string - ignoreNamespaces []string - - targetObjects []string - ignoreObjects []string - - workerRountines int -} - // Task is implemented by our namespace-collection routine, or anything else we want to run in parallel. type Task interface { Run(ctx context.Context) error @@ -217,37 +179,14 @@ func (t *dumpResourcesTask) Run(ctx context.Context) error { return nil } -func (opts *ExportOptions) validateFlags() error { - if opts.workerRountines <= 0 || opts.workerRountines > 100 { - return fmt.Errorf("invalid value %d for flag %s. Supported values are [1,100]", opts.workerRountines, workerRoutinesFlag) - } - - return nil -} - -func getRESTConfig(ctx context.Context, opts *ExportOptions) (*rest.Config, error) { - var loadingRules clientcmd.ClientConfigLoader - if opts.kubeconfig != "" { - loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: opts.kubeconfig} - } else { - loadingRules = clientcmd.NewDefaultClientConfigLoadingRules() - } - - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - loadingRules, - &clientcmd.ConfigOverrides{ - // ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}, - }).ClientConfig() -} - func RunExport(ctx context.Context, opts *ExportOptions) error { - log.Printf("Running kompanion export with kubeconfig: %s", opts.kubeconfig) + log.Printf("Running kompanion export with kubeconfig: %s", opts.Kubeconfig) if err := opts.validateFlags(); err != nil { return err } - config, err := getRESTConfig(ctx, opts) + config, err := utils.GetRESTConfig(ctx, opts.Kubeconfig) if err != nil { return fmt.Errorf("error building kubeconfig: %w", err) } @@ -284,7 +223,7 @@ func RunExport(ctx context.Context, opts *ExportOptions) error { return fmt.Errorf("error fetching namespaces: %w", err) } - reportName := timestampedName(opts.reportNamePrefix) + reportName := timestampedName(opts.ReportNamePrefix) reportTempDir := filepath.Join(".", reportName) reportFile := filepath.Join(".", reportName+".tar.gz") if err := os.Mkdir(filepath.Join(reportTempDir), 0o700); err != nil { @@ -292,10 +231,10 @@ func RunExport(ctx context.Context, opts *ExportOptions) error { } shouldExcludeObject := func(id types.NamespacedName) bool { - if shouldExclude(id.Namespace, opts.ignoreNamespaces, opts.targetNamespaces) { + if shouldExclude(id.Namespace, opts.IgnoreNamespaces, opts.TargetNamespaces) { return true } - return shouldExclude(id.Name, opts.ignoreObjects, opts.targetObjects) + return shouldExclude(id.Name, opts.IgnoreObjects, opts.TargetObjects) } // create the work log for go routine workers to use @@ -303,10 +242,10 @@ func RunExport(ctx context.Context, opts *ExportOptions) error { // Parallize across resources, unless we are scoped to a few namespaces // The thought is that if users target a particular namespace (or a few), they may not have cluster-wide permission. - perNamespace := len(opts.targetNamespaces) > 0 + perNamespace := len(opts.TargetNamespaces) > 0 if perNamespace { for _, ns := range namespaces.Items { - if shouldExclude(ns.Name, opts.ignoreNamespaces, opts.targetNamespaces) { + if shouldExclude(ns.Name, opts.IgnoreNamespaces, opts.TargetNamespaces) { continue } @@ -335,7 +274,7 @@ func RunExport(ctx context.Context, opts *ExportOptions) error { var errs []error var errsMutex sync.Mutex - for i := 0; i < opts.workerRountines; i++ { + for i := 0; i < opts.WorkerRoutines; i++ { wg.Add(1) go func() { defer wg.Done() diff --git a/experiments/kompanion/cmd/export/options.go b/experiments/kompanion/cmd/export/options.go new file mode 100644 index 0000000000..83b7046569 --- /dev/null +++ b/experiments/kompanion/cmd/export/options.go @@ -0,0 +1,59 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package export + +import ( + "log" + + "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/pkg/utils" + "github.com/spf13/pflag" +) + +const ( + reportNamePrefixFlag = "report-prefix" +) + +type ExportOptions struct { + utils.ClusterCrawlOptions + + ReportNamePrefix string +} + +func (opts *ExportOptions) AddFlags(flags *pflag.FlagSet) { + opts.ClusterCrawlAddFlags(flags) + + flags.StringVarP(&opts.ReportNamePrefix, reportNamePrefixFlag, "", opts.ReportNamePrefix, "Prefix for the report name. The tool appends a timestamp to this in the format \"YYYYMMDD-HHMMSS.milliseconds\".") +} + +func (opts *ExportOptions) validateFlags() error { + if err := opts.ValidateClusterCrawlFlags(); err != nil { + return err + } + + return nil +} + +func (opts *ExportOptions) Print() { + opts.ClusterCrawlPrint() + log.Printf("reportNamePrefix set to %q.\n", opts.ReportNamePrefix) +} + +func NewExportOptions() *ExportOptions { + opts := ExportOptions{ + ReportNamePrefix: "report", + } + opts.ClusterCrawlOptions = utils.NewClusterCrawlOptions() + return &opts +} diff --git a/experiments/kompanion/cmd/migrator/migrator.go b/experiments/kompanion/cmd/migrator/migrator.go new file mode 100644 index 0000000000..8226a11bbe --- /dev/null +++ b/experiments/kompanion/cmd/migrator/migrator.go @@ -0,0 +1,369 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package migrator + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/pkg/utils" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +const ( + examples = ` + # export Config Connector resources across all namespaces, excludes \"kube\" namespaces by default + kompanion migrator + + # exclude certain namespace prefixes + kompanion migrator --exclude-namespaces=kube --exclude-namespaces=my-team + + # target only specific namespace prefixes + kompanion migrator --target-namespaces=my-team + + # target only specific namespace prefixes AND specific object prefixes + kompanion migrator --target-namespaces=my-team --target-objects=logging + ` +) + +func BuildMigratorCmd() *cobra.Command { + opts := NewMigratorOptions() + cmd := &cobra.Command{ + Use: "migrator", + Short: "migrates Config Connector resources", + Example: examples, + RunE: func(cmd *cobra.Command, args []string) error { + return RunMigrator(cmd.Context(), opts) + }, + Args: cobra.ExactArgs(0), + } + + flags := cmd.Flags() + opts.AddFlags(flags) + + return cmd +} + +// Task is implemented by our namespace-collection routine, or anything else we want to run in parallel. +type Task interface { + Run(ctx context.Context) error +} + +// tracks the namespaces to be exported. +// thread safe. +type taskQueue struct { + mu sync.Mutex + tasks []Task // will be treated as a FIFO queue +} + +func (n *taskQueue) GetWork() Task { + n.mu.Lock() + defer n.mu.Unlock() + + if len(n.tasks) == 0 { + return nil + } + + workItem := n.tasks[0] + n.tasks = n.tasks[1:] + + return workItem +} + +func (n *taskQueue) AddTask(t Task) { + n.mu.Lock() + defer n.mu.Unlock() + + n.tasks = append(n.tasks, t) +} + +type dumpResourcesTask struct { + // Namespace is the namespace to filter down + Namespace string + + DynamicClient *dynamic.DynamicClient + + OutputChannel chan string + + // Resources is the list of resources to query + Resources []schema.GroupVersionResource + + ShouldExcludeObject func(id types.NamespacedName) bool +} + +func (t *dumpResourcesTask) Run(ctx context.Context) error { + for _, gvr := range t.Resources { + var resources *unstructured.UnstructuredList + if t.Namespace != "" { + r, err := t.DynamicClient.Resource(gvr).Namespace(t.Namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("fetching gvr %s resources in namespace %s: %w", gvr, t.Namespace, err) + } + resources = r + + } else { + r, err := t.DynamicClient.Resource(gvr).List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("fetching gvr %s resources: %w", gvr, err) + } + resources = r + } + + for _, r := range resources.Items { + id := types.NamespacedName{ + Namespace: r.GetNamespace(), + Name: r.GetName(), + } + if t.ShouldExcludeObject(id) { + continue + } + r = pruneResource(r) + + data, err := yaml.Marshal(r) + if err != nil { + return fmt.Errorf("error marshalling resource %s: %w", id, err) + } + t.OutputChannel <- string(data) + } + } + + return nil +} + +func pruneResource(r unstructured.Unstructured) unstructured.Unstructured { + newAnnotations := make(map[string]string) + annotations := r.GetAnnotations() + for key, value := range annotations { + if !strings.Contains(key, "cnrm.cloud.google.com") { + continue + } + newAnnotations[key] = value + } + if len(newAnnotations) == 0 { + r.SetAnnotations(nil) + } else { + r.SetAnnotations(newAnnotations) + } + + r.SetCreationTimestamp(metav1.Time{}) + r.SetFinalizers(nil) + r.SetGeneration(0) + r.SetManagedFields(nil) + r.SetResourceVersion("") + r.SetUID("") + + unstructured.RemoveNestedField(r.Object, "status", "conditions") + unstructured.RemoveNestedField(r.Object, "status", "creationTimestamp") + unstructured.RemoveNestedField(r.Object, "status", "healthy") + unstructured.RemoveNestedField(r.Object, "status", "observedGeneration") + unstructured.RemoveNestedField(r.Object, "status", "selfLink") // Are we sure? + status, present, err := unstructured.NestedMap(r.Object, "status") + if err != nil { + log.Fatalf("Could not get the status field %v\r", err) + } + if present && len(status) == 0 { + unstructured.RemoveNestedField(r.Object, "status") + } + + return r +} + +func RunMigrator(ctx context.Context, opts *MigratorOptions) error { + log.Printf("Running kompanion export with kubeconfig: %s", opts.Kubeconfig) + + if err := opts.validateFlags(); err != nil { + return err + } + + config, err := utils.GetRESTConfig(ctx, opts.Kubeconfig) + if err != nil { + return fmt.Errorf("error building kubeconfig: %w", err) + } + + // We rely more on server-side rate limiting now, so give it a high client-side QPS + if config.QPS == 0 { + config.QPS = 100 + config.Burst = 20 + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("error creating Kubernetes clientset: %sw", err) + } + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return fmt.Errorf("error creating dynamic client: %w", err) + } + + // use the discovery client to iterate over all api resources + discoveryClient := clientset.Discovery() + var resources []schema.GroupVersionResource + resources, err = utils.GetResources(discoveryClient, resources) + if err != nil { + return fmt.Errorf("error fetching resources: %w", err) + } + + outputChannel := make(chan string, 2*opts.WorkerRoutines) + + namespaces, err := clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("error fetching namespaces: %w", err) + } + shouldExcludeObject := func(id types.NamespacedName) bool { + if shouldExclude(id.Namespace, opts.IgnoreNamespaces, opts.TargetNamespaces) { + return true + } + return shouldExclude(id.Name, opts.IgnoreObjects, opts.TargetObjects) + } + + // write the namespaces to the migrator file + filedir := filepath.Dir(opts.MigrationFile) + if err := os.MkdirAll(filedir, 0o700); err != nil { + return fmt.Errorf("error creating directory %s: %w", filedir, err) + } + migfile, err := os.Create(opts.MigrationFile) + if err != nil { + return fmt.Errorf("error creating migration file %s: %w", opts.MigrationFile, err) + } + defer migfile.Close() + nsGvr := schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "namespaces", + } + nss, err := dynamicClient.Resource(nsGvr).List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("fetching namespace %s resources: %w", nsGvr, err) + } + for _, ns := range nss.Items { + ns = pruneResource(ns) + if len(ns.GetAnnotations()) <= 0 { + continue + } + data, err := yaml.Marshal(ns) + if err != nil { + return fmt.Errorf("error marshalling namespace: %w", err) + } + migfile.WriteString(string(data[:])) + migfile.WriteString("---\n") + } + + // create the work log for go routine workers to use + q := &taskQueue{} + + // Parallize across resources, unless we are scoped to a few namespaces + // The thought is that if users target a particular namespace (or a few), they may not have cluster-wide permission. + perNamespace := len(opts.TargetNamespaces) > 0 + if perNamespace { + for _, ns := range namespaces.Items { + if shouldExclude(ns.Name, opts.IgnoreNamespaces, opts.TargetNamespaces) { + continue + } + + q.AddTask(&dumpResourcesTask{ + Namespace: ns.Name, + Resources: resources, + DynamicClient: dynamicClient, + OutputChannel: outputChannel, + // ReportDir: reportTempDir, + ShouldExcludeObject: shouldExcludeObject, + }) + } + } else { + for _, resource := range resources { + q.AddTask(&dumpResourcesTask{ + Resources: []schema.GroupVersionResource{resource}, + DynamicClient: dynamicClient, + OutputChannel: outputChannel, + // ReportDir: reportTempDir, + ShouldExcludeObject: shouldExcludeObject, + }) + } + } + + var wg sync.WaitGroup + + var errs []error + var errsMutex sync.Mutex + + for i := 0; i < opts.WorkerRoutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + for { + task := q.GetWork() + if task == nil { + // no work + return + } + + if err := task.Run(ctx); err != nil { + errsMutex.Lock() + errs = append(errs, err) + errsMutex.Unlock() + } + } + }() + } + + go func() { + for data := range outputChannel { + migfile.WriteString(data) + migfile.WriteString("---\n") + } + }() + + wg.Wait() + close(outputChannel) + return nil +} + +func shouldExclude(name string, excludes []string, includes []string) bool { + for _, exclude := range excludes { + if strings.Contains(name, exclude) { + log.Printf("Excluding %s as it contains %s", name, exclude) + return true + } + } + + if len(includes) == 0 { + return false // no includes means includes all in this case + } + + for _, include := range includes { + if strings.Contains(name, include) { + log.Printf("Including %s as it contains %s", name, include) + return false + } + } + + // by default exclude if nothing that has been defined included this namespace. + log.Printf("Excluding %s as nothing targets it %s", name, includes) + return true +} diff --git a/experiments/kompanion/cmd/migrator/options.go b/experiments/kompanion/cmd/migrator/options.go new file mode 100644 index 0000000000..3a7f3dd59b --- /dev/null +++ b/experiments/kompanion/cmd/migrator/options.go @@ -0,0 +1,71 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package migrator + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/pkg/utils" + "github.com/spf13/pflag" +) + +const ( + migrationFileFlag = "migration-file" +) + +type MigratorOptions struct { + utils.ClusterCrawlOptions + + MigrationFile string +} + +func (opts *MigratorOptions) AddFlags(flags *pflag.FlagSet) { + opts.ClusterCrawlAddFlags(flags) + + flags.StringVarP(&opts.MigrationFile, migrationFileFlag, "", opts.MigrationFile, "path of the migration file to create.") +} + +func (opts *MigratorOptions) validateFlags() error { + if err := opts.ValidateClusterCrawlFlags(); err != nil { + return err + } + if opts.MigrationFile == "" { + return fmt.Errorf("%s is a required field", migrationFileFlag) + } + filedir := filepath.Dir(opts.MigrationFile) + if _, err := os.Stat(filedir); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("error checking migration file %s, got %v", opts.MigrationFile, err) + } + if _, err := os.Stat(opts.MigrationFile); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("error checking migration file %s, got %v", opts.MigrationFile, err) + } + return nil +} + +func (opts *MigratorOptions) Print() { + opts.ClusterCrawlPrint() + log.Printf("migrationfile set to %q.\n", opts.MigrationFile) +} + +func NewMigratorOptions() *MigratorOptions { + opts := MigratorOptions{ + MigrationFile: "migration.yaml", + } + opts.ClusterCrawlOptions = utils.NewClusterCrawlOptions() + return &opts +} diff --git a/experiments/kompanion/cmd/summary/options.go b/experiments/kompanion/cmd/summary/options.go index 6a038c9f72..66ad83270d 100644 --- a/experiments/kompanion/cmd/summary/options.go +++ b/experiments/kompanion/cmd/summary/options.go @@ -15,74 +15,32 @@ package summary import ( - "fmt" - "log" - + "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/pkg/utils" "github.com/spf13/pflag" ) -const ( - // flag names. - kubeconfigFlag = "kubeconfig" - reportNamePrefixFlag = "report-prefix" - - targetNamespacesFlag = "target-namespaces" - ignoreNamespacesFlag = "exclude-namespaces" - - targetObjectsFlag = "target-objects" - - workerRoutinesFlag = "worker-routines" -) - type SummaryOptions struct { - kubeconfig string - reportNamePrefix string - - targetNamespaces []string - ignoreNamespaces []string - - targetObjects []string - - workerRountines int + utils.ClusterCrawlOptions } -func (o *SummaryOptions) AddFlags(flags *pflag.FlagSet) { - flags.StringVarP(&o.kubeconfig, kubeconfigFlag, "", o.kubeconfig, "path to the kubeconfig file.") - flags.StringVarP(&o.reportNamePrefix, reportNamePrefixFlag, "", o.reportNamePrefix, "Prefix for the report name. The tool appends a timestamp to this in the format \"YYYYMMDD-HHMMSS.milliseconds\".") - - flags.StringArrayVarP(&o.targetNamespaces, targetNamespacesFlag, "", o.targetNamespaces, "namespace prefix to target the export tool. Targets all if empty. Can be specified multiple times.") - flags.StringArrayVarP(&o.ignoreNamespaces, ignoreNamespacesFlag, "", o.ignoreNamespaces, "namespace prefix to ignore. Excludes nothing if empty. Can be specified multiple times. Defaults to \"kube\".") - - flags.StringArrayVarP(&o.targetObjects, targetObjectsFlag, "", o.targetObjects, "object name prefix to target. Targets all if empty. Can be specified multiple times.") - - flags.IntVarP(&o.workerRountines, workerRoutinesFlag, "", o.workerRountines, "Configure the number of worker routines to export namespaces with. Defaults to 10. ") +func (opts *SummaryOptions) AddFlags(flags *pflag.FlagSet) { + opts.ClusterCrawlAddFlags(flags) } func (opts *SummaryOptions) validateFlags() error { - if opts.workerRountines <= 0 || opts.workerRountines > 100 { - return fmt.Errorf("invalid value %d for flag %s. Supported values are [1,100]", opts.workerRountines, workerRoutinesFlag) + if err := opts.ValidateClusterCrawlFlags(); err != nil { + return err } return nil } -func (o *SummaryOptions) Print() { - log.Printf("kubeconfig set to %q.\n", o.kubeconfig) - log.Printf("reportNamePrefix set to %q.\n", o.reportNamePrefix) - log.Printf("targetNamespaces set to %v.\n", o.targetNamespaces) - log.Printf("ignoreNamespaces set to %v.\n", o.ignoreNamespaces) - log.Printf("targetObjects set to %v.\n", o.targetObjects) - log.Printf("workerRountines set to %d.\n", o.workerRountines) +func (opts *SummaryOptions) Print() { + opts.ClusterCrawlPrint() } func NewSummaryOptions() *SummaryOptions { - o := SummaryOptions{ - kubeconfig: "", - reportNamePrefix: "report", - targetNamespaces: []string{}, - ignoreNamespaces: []string{"kube"}, - targetObjects: []string{}, - workerRountines: 10, - } - return &o + opts := SummaryOptions{} + opts.ClusterCrawlOptions = utils.NewClusterCrawlOptions() + return &opts } diff --git a/experiments/kompanion/cmd/summary/summary.go b/experiments/kompanion/cmd/summary/summary.go index 8980cd5db5..1ff1a7e4e5 100644 --- a/experiments/kompanion/cmd/summary/summary.go +++ b/experiments/kompanion/cmd/summary/summary.go @@ -29,8 +29,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) const ( @@ -216,30 +214,15 @@ func (t *dumpResourcesTask) Run(ctx context.Context) error { return nil } -func getRESTConfig(ctx context.Context, opts *SummaryOptions) (*rest.Config, error) { - var loadingRules clientcmd.ClientConfigLoader - if opts.kubeconfig != "" { - loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: opts.kubeconfig} - } else { - loadingRules = clientcmd.NewDefaultClientConfigLoadingRules() - } - - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - loadingRules, - &clientcmd.ConfigOverrides{ - // ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}, - }).ClientConfig() -} - func RunSummarize(ctx context.Context, opts *SummaryOptions) error { - log.Printf("Running kompanion summary with kubeconfig: %s", opts.kubeconfig) + log.Printf("Running kompanion summary with kubeconfig: %s", opts.Kubeconfig) if err := opts.validateFlags(); err != nil { opts.Print() return err } - config, err := getRESTConfig(ctx, opts) + config, err := utils.GetRESTConfig(ctx, opts.Kubeconfig) if err != nil { return fmt.Errorf("error building kubeconfig: %w", err) } @@ -280,10 +263,10 @@ func RunSummarize(ctx context.Context, opts *SummaryOptions) error { // Parallize across resources, unless we are scoped to a few namespaces // The thought is that if users target a particular namespace (or a few), they may not have cluster-wide permission. - perNamespace := len(opts.targetNamespaces) > 0 + perNamespace := len(opts.TargetNamespaces) > 0 if perNamespace { for _, ns := range namespaces.Items { - if shouldExclude(ns.Name, opts.ignoreNamespaces, opts.targetNamespaces) { + if shouldExclude(ns.Name, opts.IgnoreNamespaces, opts.TargetNamespaces) { continue } @@ -310,7 +293,7 @@ func RunSummarize(ctx context.Context, opts *SummaryOptions) error { var errs []error var errsMutex sync.Mutex - for i := 0; i < opts.workerRountines; i++ { + for i := 0; i < opts.WorkerRoutines; i++ { wg.Add(1) go func() { defer wg.Done() diff --git a/experiments/kompanion/main.go b/experiments/kompanion/main.go index ecf341d968..3bf0510b3a 100644 --- a/experiments/kompanion/main.go +++ b/experiments/kompanion/main.go @@ -19,6 +19,7 @@ import ( "os" "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/cmd/export" + "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/cmd/migrator" "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/cmd/summary" "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/pkg/version" "github.com/spf13/cobra" @@ -32,6 +33,7 @@ func BuildRootCommand() *cobra.Command { rootCmd.AddCommand(export.BuildExportCmd()) rootCmd.AddCommand(summary.BuildSummaryCmd()) + rootCmd.AddCommand(migrator.BuildMigratorCmd()) rootCmd.Version = version.GetVersion() rootCmd.CompletionOptions.DisableDefaultCmd = true diff --git a/experiments/kompanion/pkg/utils/options.go b/experiments/kompanion/pkg/utils/options.go new file mode 100644 index 0000000000..4bf6b74f77 --- /dev/null +++ b/experiments/kompanion/pkg/utils/options.go @@ -0,0 +1,90 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/pflag" +) + +const ( + // flag names. + kubeconfigFlag = "kubeconfig" + + targetNamespacesFlag = "target-namespaces" + ignoreNamespacesFlag = "exclude-namespaces" + + targetObjectsFlag = "target-objects" + + workerRoutinesFlag = "worker-routines" +) + +type ClusterCrawlOptions struct { + Kubeconfig string + + TargetNamespaces []string + IgnoreNamespaces []string + + TargetObjects []string + IgnoreObjects []string + + WorkerRoutines int +} + +func (o *ClusterCrawlOptions) ClusterCrawlAddFlags(flags *pflag.FlagSet) { + flags.StringVarP(&o.Kubeconfig, kubeconfigFlag, "", o.Kubeconfig, "path to the kubeconfig file.") + + flags.StringArrayVarP(&o.TargetNamespaces, targetNamespacesFlag, "", o.TargetNamespaces, "namespace prefix to target the export tool. Targets all if empty. Can be specified multiple times.") + flags.StringArrayVarP(&o.IgnoreNamespaces, ignoreNamespacesFlag, "", o.IgnoreNamespaces, "namespace prefix to ignore. Excludes nothing if empty. Can be specified multiple times. Defaults to \"kube\".") + + flags.StringArrayVarP(&o.TargetObjects, targetObjectsFlag, "", o.TargetObjects, "object name prefix to target. Targets all if empty. Can be specified multiple times.") + + flags.IntVarP(&o.WorkerRoutines, workerRoutinesFlag, "", o.WorkerRoutines, "Configure the number of worker routines to export namespaces with. Defaults to 10. ") +} + +func (opts *ClusterCrawlOptions) ValidateClusterCrawlFlags() error { + if opts.WorkerRoutines <= 0 || opts.WorkerRoutines > 100 { + return fmt.Errorf("invalid value %d for flag %s. Supported values are [1,100]", opts.WorkerRoutines, workerRoutinesFlag) + } + if opts.Kubeconfig != "" { + if _, err := os.Stat(opts.Kubeconfig); err != nil { + return fmt.Errorf("error reading kube config file %s, got %v", opts.Kubeconfig, err) + } + } + + return nil +} + +func (o *ClusterCrawlOptions) ClusterCrawlPrint() { + log.Printf("kubeconfig set to %q.\n", o.Kubeconfig) + log.Printf("targetNamespaces set to %v.\n", o.TargetNamespaces) + log.Printf("ignoreNamespaces set to %v.\n", o.IgnoreNamespaces) + log.Printf("targetObjects set to %v.\n", o.TargetObjects) + log.Printf("workerRountines set to %d.\n", o.WorkerRoutines) +} + +func NewClusterCrawlOptions() ClusterCrawlOptions { + o := ClusterCrawlOptions{ + Kubeconfig: "", + TargetNamespaces: []string{}, + IgnoreNamespaces: []string{"kube"}, + TargetObjects: []string{}, + WorkerRoutines: 10, + } + return o +} diff --git a/experiments/kompanion/pkg/utils/utils.go b/experiments/kompanion/pkg/utils/utils.go index f14f6c8c10..942e3c85b6 100644 --- a/experiments/kompanion/pkg/utils/utils.go +++ b/experiments/kompanion/pkg/utils/utils.go @@ -15,12 +15,15 @@ package utils import ( + "context" "fmt" "sort" "strings" "k8s.io/apimachinery/pkg/runtime/schema" - discovery "k8s.io/client-go/discovery" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" ) @@ -87,3 +90,16 @@ func contains(slice []string, str string) bool { } return false } + +func GetRESTConfig(ctx context.Context, Kubeconfig string) (*rest.Config, error) { + var loadingRules clientcmd.ClientConfigLoader + if Kubeconfig != "" { + loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: Kubeconfig} + } else { + loadingRules = clientcmd.NewDefaultClientConfigLoadingRules() + } + + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + loadingRules, + &clientcmd.ConfigOverrides{}).ClientConfig() +}