diff --git a/experiments/kompanion/Makefile b/experiments/kompanion/Makefile new file mode 100644 index 0000000000..453d23d55b --- /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 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..d0650b067e 100644 --- a/experiments/kompanion/cmd/export/export.go +++ b/experiments/kompanion/cmd/export/export.go @@ -59,59 +59,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,14 +181,6 @@ 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 != "" { @@ -335,7 +291,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..27efc073c1 --- /dev/null +++ b/experiments/kompanion/cmd/export/options.go @@ -0,0 +1,93 @@ +// 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 ( + "fmt" + "log" + + "github.com/spf13/pflag" +) + +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 + + workerRoutines int +} + +func (o *ExportOptions) 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.StringArrayVarP(&o.ignoreObjects, ignoreObjectsFlag, "", o.ignoreObjects, "object name prefix to ignore. Excludes nothing 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 *ExportOptions) validateFlags() 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) + } + + return nil +} + +func (o *ExportOptions) 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("ignoreObjects set to %v.\n", o.ignoreObjects) + log.Printf("workerRoutines set to %d.\n", o.workerRoutines) +} + +func NewExportOptions() *ExportOptions { + o := ExportOptions{ + kubeconfig: "", + reportNamePrefix: "report", + targetNamespaces: []string{}, + ignoreNamespaces: []string{"kube"}, + targetObjects: []string{}, + ignoreObjects: []string{}, + workerRoutines: 10, + } + return &o +} diff --git a/experiments/kompanion/cmd/migrator/migrator.go b/experiments/kompanion/cmd/migrator/migrator.go new file mode 100644 index 0000000000..40aae4a4ec --- /dev/null +++ b/experiments/kompanion/cmd/migrator/migrator.go @@ -0,0 +1,442 @@ +// 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" + v1 "k8s.io/api/core/v1" + 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" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +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 + + //ReportDir string + + ShouldExcludeObject func(id types.NamespacedName) bool +} + +func (t *dumpResourcesTask) Run(ctx context.Context) error { + // log := klog.FromContext(ctx) + + 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) + /* + filename := filepath.Join(t.ReportDir, id.Namespace, fmt.Sprintf("%s_%s.yaml", r.GroupVersionKind().Kind, id.Name)) + filedir := filepath.Dir(filename) + if err := os.MkdirAll(filedir, 0o700); err != nil { + return fmt.Errorf("error creating directory %s: %w", filedir, err) + } + + if err := os.WriteFile(filename, data, 0o644); err != nil { + return fmt.Errorf("error writing file for resource %s in namespace %s: %w", id.Name, id.Namespace, err) + } + */ + } + } + + return nil +} + +func pruneNamespace(ns v1.Namespace) v1.Namespace { + newAnnotations := make(map[string]string) + annotations := ns.GetAnnotations() + for key, value := range annotations { + if !strings.Contains(key, "cnrm.cloud.google.com") { + continue + } + newAnnotations[key] = value + } + if len(newAnnotations) == 0 { + ns.SetAnnotations(nil) + } else { + ns.SetAnnotations(newAnnotations) + } + + ns.SetCreationTimestamp(metav1.Time{}) + ns.SetFinalizers(nil) + ns.SetGeneration(0) + ns.SetManagedFields(nil) + ns.SetResourceVersion("") + ns.SetUID("") + + ns.Status.Conditions = nil + + return ns +} + +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 getRESTConfig(ctx context.Context, opts *MigratorOptions) (*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 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 := getRESTConfig(ctx, opts) + 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---\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() { + defer wg.Done() + + for data := range outputChannel { + migfile.WriteString(data) + migfile.WriteString("\n---\n") + } + }() + + wg.Wait() + close(outputChannel) + return nil +} + +func resourcesToName(resources []*metav1.APIResource) []string { + names := make([]string, len(resources)) + + for i, resource := range resources { + names[i] = resource.SingularName + } + + return names +} + +func shouldExclude(name string, excludes []string, includes []string) bool { + // todo acpana: maps + 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..39c34ee75a --- /dev/null +++ b/experiments/kompanion/cmd/migrator/options.go @@ -0,0 +1,104 @@ +// 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/spf13/pflag" +) + +const ( + // flag names. + kubeconfigFlag = "kubeconfig" + + migrationFileFlag = "migration-file" + + targetNamespacesFlag = "target-namespaces" + ignoreNamespacesFlag = "exclude-namespaces" + + targetObjectsFlag = "target-objects" + ignoreObjectsFlag = "exclude-objects" + + workerRoutinesFlag = "worker-routines" +) + +type MigratorOptions struct { + kubeconfig string + + migrationFile string + + targetNamespaces []string + ignoreNamespaces []string + + targetObjects []string + ignoreObjects []string + + workerRoutines int +} + +func (o *MigratorOptions) AddFlags(flags *pflag.FlagSet) { + flags.StringVarP(&o.kubeconfig, kubeconfigFlag, "", o.kubeconfig, "path to the kubeconfig file.") + + flags.StringVarP(&o.migrationFile, migrationFileFlag, "", o.migrationFile, "path of the migration file to create.") + + 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 *MigratorOptions) validateFlags() 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.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 (o *MigratorOptions) Print() { + log.Printf("kubeconfig set to %q.\n", o.kubeconfig) + log.Printf("migrationfile set to %q.\n", o.migrationFile) + 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("workerRoutines set to %d.\n", o.workerRoutines) +} + +func NewMigratorOptions() *MigratorOptions { + o := MigratorOptions{ + kubeconfig: "", + migrationFile: "migration.yaml", + targetNamespaces: []string{}, + ignoreNamespaces: []string{"kube"}, + targetObjects: []string{}, + workerRoutines: 10, + } + return &o +} diff --git a/experiments/kompanion/cmd/summary/options.go b/experiments/kompanion/cmd/summary/options.go index 6a038c9f72..1da7a66a44 100644 --- a/experiments/kompanion/cmd/summary/options.go +++ b/experiments/kompanion/cmd/summary/options.go @@ -23,8 +23,7 @@ import ( const ( // flag names. - kubeconfigFlag = "kubeconfig" - reportNamePrefixFlag = "report-prefix" + kubeconfigFlag = "kubeconfig" targetNamespacesFlag = "target-namespaces" ignoreNamespacesFlag = "exclude-namespaces" @@ -48,7 +47,6 @@ type SummaryOptions struct { 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\".") @@ -68,7 +66,6 @@ func (opts *SummaryOptions) validateFlags() error { 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) @@ -78,7 +75,6 @@ func (o *SummaryOptions) Print() { func NewSummaryOptions() *SummaryOptions { o := SummaryOptions{ kubeconfig: "", - reportNamePrefix: "report", targetNamespaces: []string{}, ignoreNamespaces: []string{"kube"}, targetObjects: []string{}, 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