diff --git a/experiments/kompanion/cmd/export/export.go b/experiments/kompanion/cmd/export/export.go index ae11d2a6b1..a6d3400ca1 100644 --- a/experiments/kompanion/cmd/export/export.go +++ b/experiments/kompanion/cmd/export/export.go @@ -24,11 +24,11 @@ import ( "log" "os" "path/filepath" - "sort" "strings" "sync" "time" + "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/pkg/utils" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -268,65 +268,14 @@ func RunExport(ctx context.Context, opts *ExportOptions) error { return fmt.Errorf("error creating dynamic client: %w", err) } - // use the discovery client to iterate over all api resoruces + // use the discovery client to iterate over all api resources discoveryClient := clientset.Discovery() - apiResourceLists, err := discoveryClient.ServerPreferredResources() - if err != nil { - return fmt.Errorf("failed to get preferred resources: %w", err) - } - var resources []schema.GroupVersionResource - - for _, apiResourceList := range apiResourceLists { - if !strings.Contains(apiResourceList.GroupVersion, ".cnrm.cloud.google.com/") { - // todo acpana log debug level - // log.Printf("ApiResource %s group doesn't contain \"cnrm\"; skipping", apiResourceList.GroupVersion) - continue - } - - apiResourceListGroupVersion, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) - if err != nil { - klog.Warningf("skipping unparseable groupVersion %q", apiResourceList.GroupVersion) - continue - } - - for _, apiResource := range apiResourceList.APIResources { - if !apiResource.Namespaced { - // todo acpana log debug level - // log.Printf("ApiResource %s is not namespaced; skipping", apiResource.SingularName) - continue - } - if !contains(apiResource.Verbs, "list") { - // todo acpana log debug level - // log.Printf("ApiResource %s is not listabble; skipping", apiResource.SingularName) - continue - } - - gvr := schema.GroupVersionResource{ - Group: apiResource.Group, - Version: apiResource.Version, - Resource: apiResource.Name, - } - - if gvr.Group == "" { - // Empty implies the group of the containing resource list. - gvr.Group = apiResourceListGroupVersion.Group - } - - if gvr.Version == "" { - // Empty implies the version of the containing resource list - gvr.Version = apiResourceListGroupVersion.Version - } - - resources = append(resources, gvr) - } + resources, err = utils.GetResources(discoveryClient, resources) + if err != nil { + return fmt.Errorf("error fetching resources: %w", err) } - // Improve determinism for debuggability and idempotency - sort.Slice(resources, func(i, j int) bool { - return resources[i].String() < resources[j].String() - }) - // todo acpana debug logs // log.Printf("Going to iterate over the following resources %+v", resourcesToName(resources)) @@ -448,16 +397,6 @@ func resourcesToName(resources []*metav1.APIResource) []string { return names } -// contains checks if a slice contains a specific string. -func contains(slice []string, str string) bool { - for _, s := range slice { - if strings.ToLower(s) == strings.ToLower(str) { - return true - } - } - return false -} - func shouldExclude(name string, excludes []string, includes []string) bool { // todo acpana: maps for _, exclude := range excludes { diff --git a/experiments/kompanion/cmd/summary/options.go b/experiments/kompanion/cmd/summary/options.go new file mode 100644 index 0000000000..6a038c9f72 --- /dev/null +++ b/experiments/kompanion/cmd/summary/options.go @@ -0,0 +1,88 @@ +// 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 summary + +import ( + "fmt" + "log" + + "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 +} + +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) 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 (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 NewSummaryOptions() *SummaryOptions { + o := SummaryOptions{ + kubeconfig: "", + reportNamePrefix: "report", + targetNamespaces: []string{}, + ignoreNamespaces: []string{"kube"}, + targetObjects: []string{}, + workerRountines: 10, + } + return &o +} diff --git a/experiments/kompanion/cmd/summary/summary.go b/experiments/kompanion/cmd/summary/summary.go new file mode 100644 index 0000000000..8980cd5db5 --- /dev/null +++ b/experiments/kompanion/cmd/summary/summary.go @@ -0,0 +1,398 @@ +// 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 summary + +import ( + "context" + "fmt" + "log" + "strings" + "sync" + + "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/pkg/utils" + "github.com/spf13/cobra" + 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 = ` + # Summarize Config Connector resources across all namespaces, excludes \"kube\" namespaces by default + kompanion summary + ` +) + +func BuildSummaryCmd() *cobra.Command { + opts := NewSummaryOptions() + cmd := &cobra.Command{ + Use: "summary", + Short: "summarize Config Connector resources", + Example: examples, + RunE: func(cmd *cobra.Command, args []string) error { + return RunSummarize(cmd.Context(), opts) + }, + Args: cobra.ExactArgs(0), + } + + flags := cmd.Flags() + opts.AddFlags(flags) + + return cmd +} + +type Result struct { + // Map[namespace, Map[gk, Map[status, count]]] + counts map[string]map[string]map[string]int + // Map[gk, Map[status, count]] + totals map[string]map[string]int + // secures access to counts & totals + countLock sync.Mutex +} + +func (r *Result) increment(namespace string, gk string, status string) { + r.countLock.Lock() + defer r.countLock.Unlock() + if r.counts == nil { + r.counts = make(map[string]map[string]map[string]int) + } + nsMap, present := r.counts[namespace] + if !present { + nsMap = make(map[string]map[string]int) + r.counts[namespace] = nsMap + } + gkMap, present := nsMap[gk] + if !present { + gkMap = make(map[string]int) + nsMap[gk] = gkMap + } + count := gkMap[status] + // Can ignore not present because count will default to 0. + gkMap[status] = count + 1 + + if r.totals == nil { + r.totals = make(map[string]map[string]int) + } + gkMap, present = r.totals[gk] + if !present { + gkMap = make(map[string]int) + r.totals[gk] = gkMap + } + count = gkMap[status] + // Can ignore not present because count will default to 0. + gkMap[status] = count + 1 +} + +func (r *Result) Print() { + r.countLock.Lock() + defer r.countLock.Unlock() + for ns, nsMap := range r.counts { + log.Printf("Namespace: %s\n", ns) + for gk, gkMap := range nsMap { + first := true + info := "(" + for status, count := range gkMap { + if first { + info += fmt.Sprintf("%s: %d", status, count) + first = false + } else { + info += fmt.Sprintf(", %s: %d", status, count) + } + } + info += ")" + log.Printf("- GroupKind: %s %s\n", gk, info) + } + } + log.Printf("Totals: \n") + for gk, gkMap := range r.totals { + first := true + info := "(" + for status, count := range gkMap { + if first { + info += fmt.Sprintf("%s: %d", status, count) + first = false + } else { + info += fmt.Sprintf(", %s: %d", status, count) + } + } + info += ")" + log.Printf("- GroupKind: %s %s\n", gk, info) + } +} + +// 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 { + // Result accumulates the overall summary + Summary *Result + + // Namespace is the namespace to filter down + Namespace string + + DynamicClient *dynamic.DynamicClient + + // Resources is the list of resources to query + Resources []schema.GroupVersionResource +} + +func (t *dumpResourcesTask) Run(ctx context.Context) error { + // klog := 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 { + status, err := getStatus(r, gvr) + if err != nil { + log.Printf("Got error retrieving status of type gvr %s, %v", gvr, err) + } + gk := gvr.Resource + "." + gvr.Group + t.Summary.increment(r.GetNamespace(), gk, status) + } + } + + 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) + + if err := opts.validateFlags(); err != nil { + opts.Print() + 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) + } + + namespaces, err := clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("error fetching namespaces: %w", err) + } + + // create the work log for go routine workers to use + q := &taskQueue{} + + summary := &Result{} + + // 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{ + Summary: summary, + Namespace: ns.Name, + Resources: resources, + DynamicClient: dynamicClient, + }) + } + } else { + for _, resource := range resources { + q.AddTask(&dumpResourcesTask{ + Summary: summary, + Resources: []schema.GroupVersionResource{resource}, + DynamicClient: dynamicClient, + }) + } + } + + log.Printf("Starting worker threads to process Config Connector objects") + var wg sync.WaitGroup + + var errs []error + var errsMutex sync.Mutex + + for i := 0; i < opts.workerRountines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + for { + task := q.GetWork() + if task == nil { + // no work + return + } + + //log.Printf("Starting work on %v", task) + if err := task.Run(ctx); err != nil { + errsMutex.Lock() + errs = append(errs, err) + errsMutex.Unlock() + } + } + }() + } + + log.Printf("Dumping Config Connector summaries") + wg.Wait() + + summary.Print() + + return nil +} + +func getStatus(r unstructured.Unstructured, gvr schema.GroupVersionResource) (string, error) { + id := types.NamespacedName{ + Namespace: r.GetNamespace(), + Name: r.GetName(), + } + slices, present, err := unstructured.NestedSlice(r.Object, "status", "conditions") + if !present { + return "no status", nil + } + if err != nil { + return "error", fmt.Errorf("error retrieving .status.healthy from resource %s: %w", id, err) + } + if len(slices) < 1 { + return "no status", nil + } else if len(slices) > 1 { + return "unknown", nil + } + element := slices[0] + condition, ok := element.(map[string]interface{}) + if !ok { + return "unknown", fmt.Errorf("resource %s of type gvr %s failed to cast %T to condition", id, gvr, element) + } + val, present, err := unstructured.NestedString(condition, "status") + if !present { + return "unknown", nil + } + if err != nil { + return "unknown", fmt.Errorf("error retrieving .status.healthy from resource %s: %w", id, err) + } + return val, nil +} + +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/go.mod b/experiments/kompanion/go.mod index 56b3195dbf..e05e4b9b96 100644 --- a/experiments/kompanion/go.mod +++ b/experiments/kompanion/go.mod @@ -13,6 +13,7 @@ require ( ) require ( + github.com/GoogleCloudPlatform/k8s-config-connector v1.124.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.10.2 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -39,11 +40,11 @@ require ( github.com/onsi/gomega v1.27.10 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.9.0 // indirect - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.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 golang.org/x/time v0.6.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/experiments/kompanion/go.sum b/experiments/kompanion/go.sum index 807493558e..1c9cb0b14a 100644 --- a/experiments/kompanion/go.sum +++ b/experiments/kompanion/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GoogleCloudPlatform/k8s-config-connector v1.124.0 h1:PD/uvvU8lqsj23FPYF5EvAV3O6enNeoB5HhE9Ix3AZI= +github.com/GoogleCloudPlatform/k8s-config-connector v1.124.0/go.mod h1:Rgm2TLgSnXVCrgIfJIizeMda37DwPQ+XhQgSgJ3L9dY= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= @@ -160,6 +162,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= @@ -181,15 +185,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/experiments/kompanion/main.go b/experiments/kompanion/main.go index 04b8db5d80..ecf341d968 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/summary" "github.com/GoogleCloudPlatform/k8s-config-connector/experiments/kompanion/pkg/version" "github.com/spf13/cobra" ) @@ -30,6 +31,7 @@ func BuildRootCommand() *cobra.Command { } rootCmd.AddCommand(export.BuildExportCmd()) + rootCmd.AddCommand(summary.BuildSummaryCmd()) rootCmd.Version = version.GetVersion() rootCmd.CompletionOptions.DisableDefaultCmd = true diff --git a/experiments/kompanion/pkg/utils/utils.go b/experiments/kompanion/pkg/utils/utils.go new file mode 100644 index 0000000000..6040a985f0 --- /dev/null +++ b/experiments/kompanion/pkg/utils/utils.go @@ -0,0 +1,89 @@ +// Copyright 2022 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" + "sort" + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" + discovery "k8s.io/client-go/discovery" + "k8s.io/klog/v2" +) + +func GetResources(discoveryClient discovery.DiscoveryInterface, resources []schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { + apiResourceLists, err := discoveryClient.ServerPreferredResources() + if err != nil { + return nil, fmt.Errorf("failed to get preferred resources: %w", err) + } + + for _, apiResourceList := range apiResourceLists { + if !strings.Contains(apiResourceList.GroupVersion, ".cnrm.cloud.google.com/") { + + continue + } + + apiResourceListGroupVersion, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) + if err != nil { + klog.Warningf("skipping unparseable groupVersion %q", apiResourceList.GroupVersion) + continue + } + + for _, apiResource := range apiResourceList.APIResources { + if !apiResource.Namespaced { + + continue + } + if !contains(apiResource.Verbs, "list") { + + continue + } + + gvr := schema.GroupVersionResource{ + Group: apiResource.Group, + Version: apiResource.Version, + Resource: apiResource.Name, + } + + if gvr.Group == "" { + + gvr.Group = apiResourceListGroupVersion.Group + } + + if gvr.Version == "" { + + gvr.Version = apiResourceListGroupVersion.Version + } + + resources = append(resources, gvr) + } + } + // Improve determinism for debuggability and idempotency + sort.Slice(resources, func(i, j int) bool { + return resources[i].String() < resources[j].String() + }) + return resources, nil +} + +// contains checks if a slice contains a specific string. +func contains(slice []string, str string) bool { + for _, s := range slice { + if strings.ToLower(s) == strings.ToLower(str) { + return true + } + } + return false +}