diff --git a/README.md b/README.md index 9024b1a1..bd317012 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identi - StorageClasses - NetworkPolicies - RoleBindings +- Argo Rollouts +- Argo Rollouts Analysis template +- Argo Rollouts Cluster Analysis template ![Kor Screenshot](/images/show_reason_screenshot.png) @@ -125,6 +128,7 @@ Kor provides various subcommands to identify and list unused resources. The avai - `daemonset`- Gets unused DaemonSets for the specified namespace or all namespaces. - `finalizer` - Gets unused pending deletion resources for the specified namespace or all namespaces. - `networkpolicy` - Gets unused NetworkPolicies for the specified namespace or all namespaces. +- `argo-rollouts, argo-rollouts-analysis-templates, argo-rollouts-cluster-analysis-templates` - Gets unsed argo-rollouts, analysis-templates and cluster-analysis-templates - `exporter` - Export Prometheus metrics. - `version` - Print kor version information. @@ -147,7 +151,9 @@ Kor provides various subcommands to identify and list unused resources. The avai --slack-auth-token string Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set. --slack-channel string Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set. --slack-webhook-url string Slack webhook URL to send notifications to + --include-third-party-crds To get unused argo-rollouts, analysis-templates and cluster-analysis-templates -v, --verbose Verbose output (print empty namespaces) + ``` To use a specific subcommand, run `kor [subcommand] [flags]`. @@ -156,6 +162,13 @@ To use a specific subcommand, run `kor [subcommand] [flags]`. kor all --include-namespaces my-namespace ``` +To get all with Argo Rollouts + +```sh +kor all --include-namespaces my-namespace --include-third-party-crds argo-rollouts,argo-rollouts-analysis-templates,argo-rollouts-cluster-analysis-templates +``` + + For more information about each subcommand and its available flags, you can use the `--help` flag. ```sh @@ -179,13 +192,16 @@ kor [subcommand] --help | Ingresses | Ingresses not pointing at any Service | | | Hpas | HPAs not used in Deployments
HPAs not used in StatefulSets | | | CRDs | CRDs not used the cluster | | -| Pvs | PVs not bound to a PVC | | -| Pdbs | PDBs not used in Deployments / StatefulSets (templates) or in arbitrary Pods
PDBs with empty selectors (match every pod) but no running pods in namespace | | +| Pvs | PVs not bound to a PVC +| Pdbs | PDBs not used in Deployments / StatefulSets (templates) or in arbitrary Pods
PDBs with empty selectors (match every pod) but no running pods in namespace | | | Jobs | Jobs status is completed
Jobs status is suspended
Jobs failed with backoff limit exceeded (including indexed jobs)
Jobs failed with dedaline exceeded | | | ReplicaSets | replicaSets that specify replicas to 0 and has already completed it's work | | DaemonSets | DaemonSets not scheduled on any nodes | -| StorageClasses | StorageClasses not used by any PVs/PVCs | +| StorageClasses | StorageClasses not used by any PVs/PVCs | NetworkPolicies | NetworkPolicies with no Pods selected by podSelector or Ingress/Egress rules | +| ArgoRollouts | ArgoRollouts not used by any deployment | +| ArgoRollouts-AnalysisTemplate | Analysys template not used by any Argo Rollout | +| ArgoRollouts-ClusterAnalysisTemplate | Cluster analysys template not used by any Argo Rollout | ### Deleting Unused resources @@ -253,6 +269,45 @@ Unused resources in namespace: "test" +---+----------------+----------------------------------------------+--------------------------------------------------------+ ``` +```sh +kor all --include-third-party-crds argo-rollouts,argo-rollouts-analysis-templates,argo-rollouts-cluster-analysis-templates --show-reason --show-reason +``` +``` +Unused resources in namespace: "default" ++---+-------------------------------+------------------------------------+------------------------------------------------+ +| # | RESOURCE TYPE | RESOURCE NAME | REASON | ++---+-------------------------------+------------------------------------+------------------------------------------------+ +| 1 | ServiceAccount | bookinfo-gateway-istio | ServiceAccount is not in use | +| 2 | ConfigMap | istio-ca-root-cert | ConfigMap is not used in any pod or container | +| 3 | Pvc | devlake-mysql-data-devlake-mysql-0 | PVC is not in use | +| 4 | ReplicaSet | rollout-canary-679b8b5b4c | ReplicaSet is not in use | +| 5 | ArgoRollout | rollout-canary | Rollout has 0 replicas | +| 6 | ArgoRollouts-AnalysisTemplate | pass | Argo Rollouts Analysis Templates is not in use | ++---+-------------------------------+------------------------------------+------------------------------------------------+ + +Unused resources in namespace: "" ++----+--------------------------------------+----------------------------------+--------------------------------------------------------------+ +| # | RESOURCE TYPE | RESOURCE NAME | REASON | ++----+--------------------------------------+----------------------------------+--------------------------------------------------------------+ +| 1 | ArgoRollouts-ClusterAnalysisTemplate | always-pass | Argo Rollouts Cluster Analysis Templates is not in use | +| 2 | ArgoRollouts-ClusterAnalysisTemplate | alert-template | Argo Rollouts Cluster Analysis Templates is not in use | +| 3 | Pv | mongo-data-pv | Persistent Volume is not in use | +| 4 | Pv | config | Persistent Volume is not in use | +| 5 | ClusterRole | cert-manager-cluster-view | ClusterRole is not used by any RoleBinding or | +| | | | ClusterRoleBinding | +| 6 | ClusterRole | cert-manager-view | ClusterRole is not used by any RoleBinding or | +| | | | ClusterRoleBinding | +| 7 | ClusterRole | cert-manager-edit | ClusterRole is not used by any RoleBinding or | +| | | | ClusterRoleBinding | +| 8 | ClusterRole | argo-rollouts-aggregate-to-admin | ClusterRole is not used by any RoleBinding or | +| | | | ClusterRoleBinding | +| 9 | ClusterRole | argo-rollouts-aggregate-to-edit | ClusterRole is not used by any RoleBinding or | +| | | | ClusterRoleBinding | +| 10 | ClusterRole | argo-rollouts-aggregate-to-view | ClusterRole is not used by any RoleBinding or | +| | | | ClusterRoleBinding | ++----+--------------------------------------+----------------------------------+--------------------------------------------------------------+ +``` + #### Group by resource ```sh @@ -283,6 +338,33 @@ Unused ReplicaSets: +---+-----------+--------------------+ ``` +```sh +kor all --include-third-party-crds argo-rollouts,argo-rollouts-analysis-templates,argo-rollouts-cluster-analysis-templates --group-by=resource --output=table +``` +```** +Unused ArgoRollouts: ++---+-----------+----------------+ +| # | NAMESPACE | RESOURCE NAME | ++---+-----------+----------------+ +| 1 | default | rollout-canary | ++---+-----------+----------------+ + +Unused ArgoRollouts-AnalysisTemplates: ++---+-----------+---------------+ +| # | NAMESPACE | RESOURCE NAME | ++---+-----------+---------------+ +| 1 | default | pass | ++---+-----------+---------------+ + +Unused ArgoRollouts-ClusterAnalysisTemplates: ++---+-----------+----------------+ +| # | NAMESPACE | RESOURCE NAME | ++---+-----------+----------------+ +| 1 | | always-pass | +| 2 | | alert-template | ++---+-----------+----------------+ +``` + #### Group by namespace ```sh diff --git a/charts/kor/Chart.yaml b/charts/kor/Chart.yaml index 76415413..f24c5d31 100644 --- a/charts/kor/Chart.yaml +++ b/charts/kor/Chart.yaml @@ -15,4 +15,4 @@ annotations: - name: Chart Source url: https://github.com/yonahd/kor/tree/main/charts/kor - name: Grafana Dashboard - url: https://grafana.com/grafana/dashboards/19863-kor-dashboard/ + url: https://grafana.com/grafana/dashboards/19863-kor-dashboard/ \ No newline at end of file diff --git a/charts/kor/values.yaml b/charts/kor/values.yaml index 4a251c0b..515dbb8d 100644 --- a/charts/kor/values.yaml +++ b/charts/kor/values.yaml @@ -85,4 +85,4 @@ serviceAccount: annotations: {} # -- The name of the service account to use. # -- If not set and create is true, a name is generated using the fullname template - name: "" + name: "" \ No newline at end of file diff --git a/cmd/kor/all.go b/cmd/kor/all.go index 3478c9be..50c887fe 100644 --- a/cmd/kor/all.go +++ b/cmd/kor/all.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -14,11 +15,11 @@ var allCmd = &cobra.Command{ Short: "Gets unused resources", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) - apiExtClient := kor.GetAPIExtensionsClient(kubeconfig) - dynamicClient := kor.GetDynamicClient(kubeconfig) - - if response, err := kor.GetUnusedAll(filterOptions, clientset, apiExtClient, dynamicClient, outputFormat, opts); err != nil { + clientset := clusterconfig.GetKubeClient(kubeconfig) + apiExtClient := clusterconfig.GetAPIExtensionsClient(kubeconfig) + dynamicClient := clusterconfig.GetDynamicClient(kubeconfig) + clientsetinterface, _ := clusterconfig.GetKubeClientForCrds(kubeconfig, clientset) + if response, err := kor.GetUnusedAll(filterOptions, clientset, apiExtClient, dynamicClient, clientsetinterface, outputFormat, opts); err != nil { fmt.Println(err) } else { utils.PrintLogo(outputFormat) diff --git a/cmd/kor/clusterroles.go b/cmd/kor/clusterroles.go index 3eca16fb..f5963267 100644 --- a/cmd/kor/clusterroles.go +++ b/cmd/kor/clusterroles.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var clusterRoleCmd = &cobra.Command{ Short: "Gets unused cluster roles", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedClusterRoles(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/configmaps.go b/cmd/kor/configmaps.go index 161b2a61..8e8b062f 100644 --- a/cmd/kor/configmaps.go +++ b/cmd/kor/configmaps.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var configmapCmd = &cobra.Command{ Short: "Gets unused configmaps", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedConfigmaps(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { diff --git a/cmd/kor/crds.go b/cmd/kor/crds.go index eebd98f3..89f8294a 100644 --- a/cmd/kor/crds.go +++ b/cmd/kor/crds.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,8 +16,8 @@ var crdCmd = &cobra.Command{ Short: "Gets unused crds", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - apiExtClient := kor.GetAPIExtensionsClient(kubeconfig) - dynamicClient := kor.GetDynamicClient(kubeconfig) + apiExtClient := clusterconfig.GetAPIExtensionsClient(kubeconfig) + dynamicClient := clusterconfig.GetDynamicClient(kubeconfig) if response, err := kor.GetUnusedCrds(filterOptions, apiExtClient, dynamicClient, outputFormat, opts); err != nil { fmt.Println(err) } else { diff --git a/cmd/kor/daemonsets.go b/cmd/kor/daemonsets.go index 374934e9..5a0f5ef5 100644 --- a/cmd/kor/daemonsets.go +++ b/cmd/kor/daemonsets.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var dsCmd = &cobra.Command{ Short: "Gets unused daemonSets", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedDaemonSets(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/deployments.go b/cmd/kor/deployments.go index 4eb16d6d..bc23f5f9 100644 --- a/cmd/kor/deployments.go +++ b/cmd/kor/deployments.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var deployCmd = &cobra.Command{ Short: "Gets unused deployments", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedDeployments(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { diff --git a/cmd/kor/exporter.go b/cmd/kor/exporter.go index bb245124..50f06308 100644 --- a/cmd/kor/exporter.go +++ b/cmd/kor/exporter.go @@ -3,6 +3,7 @@ package kor import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" ) @@ -13,11 +14,12 @@ var exporterCmd = &cobra.Command{ Short: "start prometheus exporter", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) - apiExtClient := kor.GetAPIExtensionsClient(kubeconfig) - dynamicClient := kor.GetDynamicClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) + clientsetinterface, _ := clusterconfig.GetKubeClientForCrds(kubeconfig, clientset) + apiExtClient := clusterconfig.GetAPIExtensionsClient(kubeconfig) + dynamicClient := clusterconfig.GetDynamicClient(kubeconfig) - kor.Exporter(filterOptions, clientset, apiExtClient, dynamicClient, "json", opts, resourceList) + kor.Exporter(filterOptions, clientset, apiExtClient, dynamicClient, clientsetinterface, "json", opts, resourceList) }, } diff --git a/cmd/kor/finalizers.go b/cmd/kor/finalizers.go index 08a8acd8..80d97e6f 100644 --- a/cmd/kor/finalizers.go +++ b/cmd/kor/finalizers.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" ) @@ -14,8 +15,8 @@ var finalizerCmd = &cobra.Command{ Short: "Gets resources waiting for finalizers to delete", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) - dynamicClient := kor.GetDynamicClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) + dynamicClient := clusterconfig.GetDynamicClient(kubeconfig) if response, err := kor.GetUnusedfinalizers(filterOptions, clientset, dynamicClient, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/hpas.go b/cmd/kor/hpas.go index 2679637e..859ee56e 100644 --- a/cmd/kor/hpas.go +++ b/cmd/kor/hpas.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var hpaCmd = &cobra.Command{ Short: "Gets unused hpas", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedHpas(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/ingresses.go b/cmd/kor/ingresses.go index 9ec5c91b..06f06ff7 100644 --- a/cmd/kor/ingresses.go +++ b/cmd/kor/ingresses.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var ingressCmd = &cobra.Command{ Short: "Gets unused ingresses", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedIngresses(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/jobs.go b/cmd/kor/jobs.go index 16034eb8..3792cf79 100644 --- a/cmd/kor/jobs.go +++ b/cmd/kor/jobs.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var jobCmd = &cobra.Command{ Short: "Gets unused jobs", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedJobs(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/networkpolicies.go b/cmd/kor/networkpolicies.go index cb0efea4..21758435 100644 --- a/cmd/kor/networkpolicies.go +++ b/cmd/kor/networkpolicies.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var netpolCmd = &cobra.Command{ Short: "Gets unused networkpolicies", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedNetworkPolicies(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) } else { diff --git a/cmd/kor/pdbs.go b/cmd/kor/pdbs.go index 6fecadee..84be68f9 100644 --- a/cmd/kor/pdbs.go +++ b/cmd/kor/pdbs.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var pdbCmd = &cobra.Command{ Short: "Gets unused pdbs", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedPdbs(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/pods.go b/cmd/kor/pods.go index 9c13c036..df67ebcb 100644 --- a/cmd/kor/pods.go +++ b/cmd/kor/pods.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var podCmd = &cobra.Command{ Short: "Gets unused pods", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedPods(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/pv.go b/cmd/kor/pv.go index dcfc6692..ff73478e 100644 --- a/cmd/kor/pv.go +++ b/cmd/kor/pv.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var pvCmd = &cobra.Command{ Short: "Gets unused pvs", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedPvs(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/pvc.go b/cmd/kor/pvc.go index e29f9c61..fdb03515 100644 --- a/cmd/kor/pvc.go +++ b/cmd/kor/pvc.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var pvcCmd = &cobra.Command{ Short: "Gets unused pvcs", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedPvcs(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/replicasets.go b/cmd/kor/replicasets.go index f12e8dff..6483b48f 100644 --- a/cmd/kor/replicasets.go +++ b/cmd/kor/replicasets.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var replicaSetCmd = &cobra.Command{ Short: "Gets unused replicaSets", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedReplicaSets(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/rolebindings.go b/cmd/kor/rolebindings.go index 5b471450..0fe20e77 100644 --- a/cmd/kor/rolebindings.go +++ b/cmd/kor/rolebindings.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var roleBindingCmd = &cobra.Command{ Short: "Gets unused role bindings", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedRoleBindings(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/roles.go b/cmd/kor/roles.go index 2f71739b..7471c64e 100644 --- a/cmd/kor/roles.go +++ b/cmd/kor/roles.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var roleCmd = &cobra.Command{ Short: "Gets unused roles", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedRoles(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/root.go b/cmd/kor/root.go index 17c818e9..b3cad3e0 100644 --- a/cmd/kor/root.go +++ b/cmd/kor/root.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" "github.com/yonahd/kor/pkg/kor" @@ -31,11 +32,12 @@ var rootCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { resourceNames := args[0] - clientset := kor.GetKubeClient(kubeconfig) - apiExtClient := kor.GetAPIExtensionsClient(kubeconfig) - dynamicClient := kor.GetDynamicClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) + clientsetinterface, _ := clusterconfig.GetKubeClientForCrds(kubeconfig, clientset) + apiExtClient := clusterconfig.GetAPIExtensionsClient(kubeconfig) + dynamicClient := clusterconfig.GetDynamicClient(kubeconfig) - if response, err := kor.GetUnusedMulti(resourceNames, filterOptions, clientset, apiExtClient, dynamicClient, outputFormat, opts); err != nil { + if response, err := kor.GetUnusedMulti(resourceNames, filterOptions, clientset, apiExtClient, dynamicClient, clientsetinterface, outputFormat, opts); err != nil { fmt.Println(err) } else { utils.PrintLogo(outputFormat) @@ -85,4 +87,5 @@ func addFilterOptionsFlag(cmd *cobra.Command, opts *filters.Options) { cmd.PersistentFlags().StringVar(&opts.IncludeLabels, "include-labels", opts.IncludeLabels, "Selector to filter in, Example: --include-labels key1=value1.(currently supports one label)") cmd.PersistentFlags().StringSliceVarP(&opts.ExcludeNamespaces, "exclude-namespaces", "e", opts.ExcludeNamespaces, "Namespaces to be excluded, split by commas. Example: --exclude-namespaces ns1,ns2,ns3. If --include-namespaces is set, --exclude-namespaces will be ignored.") cmd.PersistentFlags().StringSliceVarP(&opts.IncludeNamespaces, "include-namespaces", "n", opts.IncludeNamespaces, "Namespaces to run on, split by commas. Example: --include-namespaces ns1,ns2,ns3. If set, non-namespaced resources will be ignored.") + rootCmd.PersistentFlags().StringSliceVar(&opts.IncludeThirdPartyCrds, "include-third-party-crds", opts.IncludeThirdPartyCrds, "Custom resources defintions to search, split by commas. Example: --include-crds argo-rollouts. If set, the chosen crd will be returned (in addition to the other resources, of course)") } diff --git a/cmd/kor/secrets.go b/cmd/kor/secrets.go index b484d4af..0a79aaf1 100644 --- a/cmd/kor/secrets.go +++ b/cmd/kor/secrets.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var secretCmd = &cobra.Command{ Short: "Gets unused secrets", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedSecrets(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/serviceaccounts.go b/cmd/kor/serviceaccounts.go index ba73b462..faae3e15 100644 --- a/cmd/kor/serviceaccounts.go +++ b/cmd/kor/serviceaccounts.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var serviceAccountCmd = &cobra.Command{ Short: "Gets unused service accounts", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedServiceAccounts(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/services.go b/cmd/kor/services.go index 996ca611..2ce5aaad 100644 --- a/cmd/kor/services.go +++ b/cmd/kor/services.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var serviceCmd = &cobra.Command{ Short: "Gets unused services", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedServices(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/statefulsets.go b/cmd/kor/statefulsets.go index dd53e17e..98cee91d 100644 --- a/cmd/kor/statefulsets.go +++ b/cmd/kor/statefulsets.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var stsCmd = &cobra.Command{ Short: "Gets unused statefulSets", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedStatefulSets(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/cmd/kor/storageclasses.go b/cmd/kor/storageclasses.go index eade9e03..a7f2f074 100644 --- a/cmd/kor/storageclasses.go +++ b/cmd/kor/storageclasses.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/kor" "github.com/yonahd/kor/pkg/utils" ) @@ -15,7 +16,7 @@ var scCmd = &cobra.Command{ Short: "Gets unused storageClasses", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - clientset := kor.GetKubeClient(kubeconfig) + clientset := clusterconfig.GetKubeClient(kubeconfig) if response, err := kor.GetUnusedStorageClasses(filterOptions, clientset, outputFormat, opts); err != nil { fmt.Println(err) diff --git a/go.mod b/go.mod index 82a4ca30..adfd1b44 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/yonahd/kor go 1.23.2 require ( + github.com/argoproj/argo-rollouts v1.7.2 github.com/fatih/color v1.18.0 github.com/olekukonko/tablewriter v0.0.5 github.com/prometheus/client_golang v1.20.5 diff --git a/go.sum b/go.sum index 1f576778..4c9fc847 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/argoproj/argo-rollouts v1.7.2 h1:faDUH/qePerYRwsrHfVzNQkhjGBgXIiVYdVK8824kMo= +github.com/argoproj/argo-rollouts v1.7.2/go.mod h1:Te4HrUELxKiBpK8lgk77o4gTa3mv8pXCd8xdPprKrbs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -190,8 +192,6 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/pkg/clusterconfig/connection.go b/pkg/clusterconfig/connection.go new file mode 100644 index 00000000..3f7ccfe4 --- /dev/null +++ b/pkg/clusterconfig/connection.go @@ -0,0 +1,129 @@ +package clusterconfig + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" +) + +type ClientInterface interface { + GetKubernetesClient() kubernetes.Interface + GetArgoRolloutsClient() versioned.Interface +} + +type ClientSet struct { + coreClient *kubernetes.Clientset + coreClientArgoRollouts *versioned.Clientset +} + +func GetKubeConfigPath() string { + home := homedir.HomeDir() + return filepath.Join(home, ".kube", "config") +} + +func GetConfig(kubeconfig string) (*rest.Config, error) { + if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token"); err == nil { + return rest.InClusterConfig() + } + + if kubeconfig == "" { + if configEnv := os.Getenv("KUBECONFIG"); configEnv != "" { + kubeconfig = configEnv + } else { + kubeconfig = GetKubeConfigPath() + } + } + + return clientcmd.BuildConfigFromFlags("", kubeconfig) +} + +func GetKubeClient(kubeconfig string) *kubernetes.Clientset { + config, err := GetConfig(kubeconfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err) + os.Exit(1) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err) + os.Exit(1) + } + return clientset +} + +func GetAPIExtensionsClient(kubeconfig string) *apiextensionsclientset.Clientset { + config, err := GetConfig(kubeconfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err) + os.Exit(1) + } + + clientset, err := apiextensionsclientset.NewForConfig(config) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err) + os.Exit(1) + } + return clientset +} + +func GetDynamicClient(kubeconfig string) *dynamic.DynamicClient { + config, err := GetConfig(kubeconfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err) + os.Exit(1) + } + + clientset, err := dynamic.NewForConfig(config) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err) + os.Exit(1) + } + return clientset +} + +func (c ClientSet) GetArgoRolloutsClient() versioned.Interface { + return c.coreClientArgoRollouts +} + +// GetKubernetesClient returns the Kubernetes core client. +func (c ClientSet) GetKubernetesClient() kubernetes.Interface { + return c.coreClient +} + +func GetKubeClientForCrds(kubeconfig string, clientset *kubernetes.Clientset) (ClientInterface, error) { + config, err := GetConfig(kubeconfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err) + os.Exit(1) + } + clientsetall, err := NewClientSetForCrd(config, clientset) + + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err) + os.Exit(1) + } + return clientsetall, nil +} + +func NewClientSetForCrd(config *rest.Config, clientset *kubernetes.Clientset) (ClientInterface, error) { + // Create the custom v1 client + coreClientArgoRolloutsV1Client, err := versioned.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create Argo Rollouts client: %v", err) + } + + // Return the ClientSet struct + return &ClientSet{ + coreClient: clientset, + coreClientArgoRollouts: coreClientArgoRolloutsV1Client, + }, nil +} diff --git a/pkg/filters/options.go b/pkg/filters/options.go index 0244b0f1..db98afa7 100644 --- a/pkg/filters/options.go +++ b/pkg/filters/options.go @@ -39,8 +39,9 @@ type Options struct { // IncludeNamespaces is a namespace selector to include resources in matching namespaces IncludeNamespaces []string - namespace []string - once sync.Once + namespace []string + once sync.Once + IncludeThirdPartyCrds []string } // NewFilterOptions returns a new FilterOptions instance with default values @@ -160,6 +161,22 @@ func (o *Options) Namespaces(clientset kubernetes.Interface) []string { return o.namespace } +func (o *Options) CleanRepeatedCrds() []string { + if len(o.IncludeThirdPartyCrds) > 0 { + keys := make(map[string]bool) + includecrdsNew := make([]string, 0) + + for _, entry := range o.IncludeThirdPartyCrds { + if _, value := keys[entry]; !value { + keys[entry] = true + includecrdsNew = append(includecrdsNew, entry) + } + o.IncludeThirdPartyCrds = includecrdsNew + } + } + return o.IncludeThirdPartyCrds +} + func (o *Options) modifyLabels() { if o.IncludeLabels != "" { if len(o.ExcludeLabels) > 0 { diff --git a/pkg/kor/all.go b/pkg/kor/all.go index ffaad22b..bf320e50 100644 --- a/pkg/kor/all.go +++ b/pkg/kor/all.go @@ -10,6 +10,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" ) @@ -277,7 +278,7 @@ func getUnusedRoleBindings(clientset kubernetes.Interface, namespace string, fil return namespaceRoleBindingDiff } -func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts common.Opts) (string, error) { +func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, clientsetinterface clusterconfig.ClientInterface, outputFormat string, opts common.Opts) (string, error) { resources := make(map[string]map[string][]ResourceInfo) for _, namespace := range filterOpts.Namespaces(clientset) { switch opts.GroupBy { @@ -300,6 +301,8 @@ func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.In resources[namespace]["DaemonSet"] = getUnusedDaemonSets(clientset, namespace, filterOpts).diff resources[namespace]["NetworkPolicy"] = getUnusedNetworkPolicies(clientset, namespace, filterOpts).diff resources[namespace]["RoleBinding"] = getUnusedRoleBindings(clientset, namespace, filterOpts).diff + GetUnusedCrdsThirdParty(opts.GroupBy, clientsetinterface, namespace, filterOpts, resources, true) + case "resource": appendResources(resources, "ConfigMap", namespace, getUnusedCMs(clientset, namespace, filterOpts).diff) appendResources(resources, "Service", namespace, getUnusedSVCs(clientset, namespace, filterOpts).diff) @@ -318,6 +321,7 @@ func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.In appendResources(resources, "DaemonSet", namespace, getUnusedDaemonSets(clientset, namespace, filterOpts).diff) appendResources(resources, "NetworkPolicy", namespace, getUnusedNetworkPolicies(clientset, namespace, filterOpts).diff) appendResources(resources, "RoleBinding", namespace, getUnusedRoleBindings(clientset, namespace, filterOpts).diff) + GetUnusedCrdsThirdParty(opts.GroupBy, clientsetinterface, namespace, filterOpts, resources, true) } } @@ -341,7 +345,7 @@ func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.In return unusedAllNamespaced, nil } -func GetUnusedAllNonNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { +func GetUnusedAllNonNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, clientsetinterface clusterconfig.ClientInterface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { resources := make(map[string]map[string][]ResourceInfo) switch opts.GroupBy { case "namespace": @@ -350,11 +354,13 @@ func GetUnusedAllNonNamespaced(filterOpts *filters.Options, clientset kubernetes resources[""]["Pv"] = getUnusedPvs(clientset, filterOpts).diff resources[""]["ClusterRole"] = getUnusedClusterRoles(clientset, filterOpts).diff resources[""]["StorageClass"] = getUnusedStorageClasses(clientset, filterOpts).diff + GetUnusedCrdsThirdParty(opts.GroupBy, clientsetinterface, "", filterOpts, resources, false) case "resource": appendResources(resources, "Crd", "", getUnusedCrds(apiExtClient, dynamicClient, filterOpts).diff) appendResources(resources, "Pv", "", getUnusedPvs(clientset, filterOpts).diff) appendResources(resources, "ClusterRole", "", getUnusedClusterRoles(clientset, filterOpts).diff) appendResources(resources, "StorageClass", "", getUnusedStorageClasses(clientset, filterOpts).diff) + GetUnusedCrdsThirdParty(opts.GroupBy, clientsetinterface, "", filterOpts, resources, false) } var outputBuffer bytes.Buffer @@ -377,8 +383,8 @@ func GetUnusedAllNonNamespaced(filterOpts *filters.Options, clientset kubernetes return unusedAllNonNamespaced, nil } -func GetUnusedAll(filterOpts *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { - unusedAllNamespaced, err := GetUnusedAllNamespaced(filterOpts, clientset, outputFormat, opts) +func GetUnusedAll(filterOpts *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, clientsetinterface clusterconfig.ClientInterface, outputFormat string, opts common.Opts) (string, error) { + unusedAllNamespaced, err := GetUnusedAllNamespaced(filterOpts, clientset, clientsetinterface, outputFormat, opts) if err != nil { fmt.Printf("err: %v\n", err) } @@ -388,7 +394,7 @@ func GetUnusedAll(filterOpts *filters.Options, clientset kubernetes.Interface, a return unusedAllNamespaced, nil } - unusedAllNonNamespaced, err := GetUnusedAllNonNamespaced(filterOpts, clientset, apiExtClient, dynamicClient, outputFormat, opts) + unusedAllNonNamespaced, err := GetUnusedAllNonNamespaced(filterOpts, clientset, clientsetinterface, apiExtClient, dynamicClient, outputFormat, opts) if err != nil { fmt.Printf("err: %v\n", err) } @@ -424,3 +430,37 @@ func GetUnusedAll(filterOpts *filters.Options, clientset kubernetes.Interface, a return string(jsonResponse), nil } } + +func GetUnusedCrdsThirdParty(groupBy string, clientsetinterface clusterconfig.ClientInterface, namespace string, filterOpts *filters.Options, resources map[string]map[string][]ResourceInfo, namespaced bool) map[string]map[string][]ResourceInfo { + for _, crd := range filterOpts.CleanRepeatedCrds() { + if namespaced { + switch crd { + case "argo-rollouts": + unusedArgoRollouts := GetUnusedArgoRollouts(clientsetinterface, namespace, filterOpts) + separateItemsThirdParty(resources, namespace, groupBy, "ArgoRollout", unusedArgoRollouts.diff) + case "argo-rollouts-analysis-templates": + unusedArgoRolloutsAnalysisTemplates := GetUnusedArgoRolloutsAnalysisTemplates(clientsetinterface, namespace, filterOpts) + separateItemsThirdParty(resources, namespace, groupBy, "ArgoRollouts-AnalysisTemplate", unusedArgoRolloutsAnalysisTemplates.diff) + } + } + if !namespaced { + switch crd { + case "argo-rollouts-cluster-analysis-templates": + unusedArgoRolloutsClusterAnalysisTemplates := GetUnusedArgoRolloutsClusterAnalysisTemplates(clientsetinterface, "", filterOpts) + separateItemsThirdParty(resources, "", groupBy, "ArgoRollouts-ClusterAnalysisTemplate", unusedArgoRolloutsClusterAnalysisTemplates.diff) + } + } + } + return resources +} + +func separateItemsThirdParty(resources map[string]map[string][]ResourceInfo, namespace string, groupBy string, name string, diff []ResourceInfo) map[string]map[string][]ResourceInfo { + switch groupBy { + case "namespace": + resources[namespace][name] = diff + case "resource": + appendResources(resources, name, namespace, diff) + } + + return resources +} diff --git a/pkg/kor/argorollouts.go b/pkg/kor/argorollouts.go new file mode 100644 index 00000000..ef4b25d8 --- /dev/null +++ b/pkg/kor/argorollouts.go @@ -0,0 +1,214 @@ +package kor + +import ( + "context" + "fmt" + "os" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/yonahd/kor/pkg/clusterconfig" + "github.com/yonahd/kor/pkg/filters" +) + +func GetUnusedArgoRollouts(clientsetinterface clusterconfig.ClientInterface, namespace string, filterOpts *filters.Options) ResourceDiff { + argoRolloutsDiff, err := processNamespaceArgoRollouts(clientsetinterface, namespace, filterOpts) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "argorollouts", namespace, err) + } + namespaceSADiff := ResourceDiff{ + "ArgoRollouts", + argoRolloutsDiff, + } + + return namespaceSADiff +} + +func GetUnusedArgoRolloutsAnalysisTemplates(clientsetinterface clusterconfig.ClientInterface, namespace string, filterOpts *filters.Options) ResourceDiff { + argoRolloutsDiff, err := processNamespaceArgoAnalysisTemplate(clientsetinterface, namespace, filterOpts) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "analysis templates from Argo rollouts", namespace, err) + } + + namespaceSADiff := ResourceDiff{ + "Analysis Templates", + argoRolloutsDiff, + } + + return namespaceSADiff +} + +func GetUnusedArgoRolloutsClusterAnalysisTemplates(clientsetinterface clusterconfig.ClientInterface, namespace string, filterOpts *filters.Options) ResourceDiff { + argoRolloutsDiff, err := processNamespaceArgoClusterAnalysisTemplate(clientsetinterface, namespace, filterOpts) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "analysis templates from Argo rollouts", namespace, err) + } + + namespaceSADiff := ResourceDiff{ + "Cluster Analysis Templates", + argoRolloutsDiff, + } + + return namespaceSADiff +} + +func processNamespaceArgoRollouts(clientsetinterface clusterconfig.ClientInterface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) { + clientset := clientsetinterface.GetKubernetesClient() + clientsetargorollouts := clientsetinterface.GetArgoRolloutsClient() + argoRolloutList, err := clientsetargorollouts.ArgoprojV1alpha1().Rollouts(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels}) + + if err != nil { + return nil, err + } + + var argoRolloutWithoutReplicas []ResourceInfo + + for _, argoRollout := range argoRolloutList.Items { + if pass, _ := filter.SetObject(&argoRollout).Run(filterOpts); pass { + continue + } + if argoRollout.Labels["kor/used"] == "false" { + reason := "Marked with unused label" + argoRolloutWithoutReplicas = append(argoRolloutWithoutReplicas, ResourceInfo{Name: argoRollout.Name, Reason: reason}) + continue + } + deploymentWorkLoadRef := argoRollout.Spec.WorkloadRef + + if deploymentWorkLoadRef == nil { + if *argoRollout.Spec.Replicas == 0 { + reason := "Rollout has 0 replicas" + argoRolloutWithoutReplicas = append(argoRolloutWithoutReplicas, ResourceInfo{Name: argoRollout.Name, Reason: reason}) + } + } + + if deploymentWorkLoadRef != nil && deploymentWorkLoadRef.Kind == "Deployment" { + deploymentItem, _ := clientset.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentWorkLoadRef.Name, metav1.GetOptions{}) + + if deploymentItem.GetName() == "" { + reason := "Rollout has no deployments" + argoRolloutWithoutReplicas = append(argoRolloutWithoutReplicas, ResourceInfo{Name: argoRollout.Name, Reason: reason}) + } + } + } + + return argoRolloutWithoutReplicas, nil +} + +func processNamespaceArgoAnalysisTemplate(clientsetinterface clusterconfig.ClientInterface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) { + clientsetargorollouts := clientsetinterface.GetArgoRolloutsClient() + argoRolloutList, err := clientsetargorollouts.ArgoprojV1alpha1().Rollouts(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels}) + argoRolloutAnalysisTemplateList, _ := clientsetargorollouts.ArgoprojV1alpha1().AnalysisTemplates(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels}) + if err != nil { + return nil, err + } + + var analysisTemplateList []ResourceInfo + + for _, argoRolloutAnalysisTemplate := range argoRolloutAnalysisTemplateList.Items { + if argoRolloutAnalysisTemplate.Labels["kor/used"] == "false" { + reason := "Marked with unused label" + analysisTemplateList = append(analysisTemplateList, ResourceInfo{Name: argoRolloutAnalysisTemplate.Name, Reason: reason}) + continue + } + } + for _, argoRolloutAnalysisTemplate := range argoRolloutAnalysisTemplateList.Items { + templateNameInUse := false + for _, argoRollout := range argoRolloutList.Items { + if pass, _ := filter.SetObject(&argoRollout).Run(filterOpts); pass { + continue + } + + skip := argoRollout.Spec.Strategy.Canary == nil || argoRollout.Spec.Strategy.Canary.Analysis == nil || len(argoRollout.Spec.Strategy.Canary.Analysis.Templates) < 1 + if !skip { + rolloutCanaryAnalysis := argoRollout.Spec.Strategy.Canary.Analysis.Templates + for _, canaryAnalysisItem := range rolloutCanaryAnalysis { + templateNameInUse = canaryAnalysisItem.TemplateName == argoRolloutAnalysisTemplate.Name + if templateNameInUse { + continue + } + } + } + + skip = argoRollout.Spec.Strategy.BlueGreen == nil || argoRollout.Spec.Strategy.BlueGreen.PrePromotionAnalysis == nil || len(argoRollout.Spec.Strategy.BlueGreen.PrePromotionAnalysis.Templates) < 1 + if !skip { + rolloutBlueGreenAnalysis := argoRollout.Spec.Strategy.BlueGreen.PrePromotionAnalysis.Templates + for _, blueGreenAnalysisAnalysisItem := range rolloutBlueGreenAnalysis { + for _, argoRolloutAnalysisTemplate := range argoRolloutAnalysisTemplateList.Items { + templateNameInUse = blueGreenAnalysisAnalysisItem.TemplateName == argoRolloutAnalysisTemplate.Name + if templateNameInUse { + continue + } + } + } + } + } + if !templateNameInUse { + reason := "Argo Rollouts Analysis Templates is not in use" + analysisTemplateList = append(analysisTemplateList, ResourceInfo{Name: argoRolloutAnalysisTemplate.Name, Reason: reason}) + } + } + return analysisTemplateList, nil +} + +func processNamespaceArgoClusterAnalysisTemplate(clientsetinterface clusterconfig.ClientInterface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) { + clientset := clientsetinterface.GetKubernetesClient() + clientsetargorollouts := clientsetinterface.GetArgoRolloutsClient() + + var clusterAnalysisTemplateList []ResourceInfo + + argoRolloutAnalysisTemplateList, _ := clientsetargorollouts.ArgoprojV1alpha1().ClusterAnalysisTemplates().List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels}) + for _, argoRolloutAnalysisTemplate := range argoRolloutAnalysisTemplateList.Items { + if argoRolloutAnalysisTemplate.Labels["kor/used"] == "false" { + reason := "Marked with unused label" + clusterAnalysisTemplateList = append(clusterAnalysisTemplateList, ResourceInfo{Name: argoRolloutAnalysisTemplate.Name, Reason: reason}) + continue + } + } + + for _, namespace := range filterOpts.Namespaces(clientset) { + argoRolloutList, _ := clientsetargorollouts.ArgoprojV1alpha1().Rollouts(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels}) + + for _, argoRolloutAnalysisTemplate := range argoRolloutAnalysisTemplateList.Items { + templateNameInUse := false + for _, argoRollout := range argoRolloutList.Items { + if pass, _ := filter.SetObject(&argoRollout).Run(filterOpts); pass { + continue + } + + templateNameInUse = false + skip := argoRollout.Spec.Strategy.Canary == nil || argoRollout.Spec.Strategy.Canary.Analysis == nil || len(argoRollout.Spec.Strategy.Canary.Analysis.Templates) < 1 + if !skip { + rolloutCanaryAnalysis := argoRollout.Spec.Strategy.Canary.Analysis.Templates + for _, canaryAnalysisItem := range rolloutCanaryAnalysis { + if canaryAnalysisItem.TemplateName == argoRolloutAnalysisTemplate.Name { + templateNameInUse = true + continue + } + + } + } + + skip = argoRollout.Spec.Strategy.BlueGreen == nil || argoRollout.Spec.Strategy.BlueGreen.PrePromotionAnalysis == nil || len(argoRollout.Spec.Strategy.BlueGreen.PrePromotionAnalysis.Templates) < 1 + if !skip { + rolloutBlueGreenAnalysis := argoRollout.Spec.Strategy.BlueGreen.PrePromotionAnalysis.Templates + for _, blueGreenAnalysisAnalysisItem := range rolloutBlueGreenAnalysis { + for _, argoRolloutAnalysisTemplate := range argoRolloutAnalysisTemplateList.Items { + if blueGreenAnalysisAnalysisItem.TemplateName == argoRolloutAnalysisTemplate.Name { + templateNameInUse = true + continue + } + } + } + } + } + if !templateNameInUse { + reason := "Argo Rollouts Cluster Analysis Templates is not in use" + if !SkipIfContainsValue(clusterAnalysisTemplateList, "Name", argoRolloutAnalysisTemplate.Name) { + clusterAnalysisTemplateList = append(clusterAnalysisTemplateList, ResourceInfo{Name: argoRolloutAnalysisTemplate.Name, Reason: reason}) + } + } + } + } + + return clusterAnalysisTemplateList, nil +} diff --git a/pkg/kor/argorollouts_test.go b/pkg/kor/argorollouts_test.go new file mode 100644 index 00000000..3e67b57c --- /dev/null +++ b/pkg/kor/argorollouts_test.go @@ -0,0 +1,434 @@ +package kor + +import ( + "bytes" + "context" + "encoding/json" + "reflect" + "testing" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + + "github.com/yonahd/kor/pkg/clusterconfig" + "github.com/yonahd/kor/pkg/common" + "github.com/yonahd/kor/pkg/filters" +) + +func createTestArgoRolloutMultiResources(t *testing.T, rolloutName string, implementationType string) (kubernetes.Interface, clusterconfig.ClientInterface, *appsv1.Deployment) { + clientsetinterface, _ := NewFakeClientSet(t) + clientset := clientsetinterface.GetKubernetesClient() + clientsetargorollouts := clientsetinterface.GetArgoRolloutsClient() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + deploymentName := "test-deployment1" + + deployment := CreateTestDeployment(testNamespace, deploymentName, 0, AppLabels) + _, err = clientset.AppsV1().Deployments(testNamespace).Create(context.TODO(), deployment, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake deployment: %v", err) + } + + rollout := CreateTestArgoRolloutWithDeployment(testNamespace, rolloutName, deployment, AppLabels, implementationType) + + _, err = clientsetargorollouts.ArgoprojV1alpha1().Rollouts(testNamespace).Create(context.TODO(), rollout, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake argo rollout: %v", err) + } + + return clientset, clientsetinterface, deployment + +} + +func createTestArgoRolloutMultiResourcesWithAnalysis(t *testing.T, rolloutName string, analysisName string, implementationType string) (kubernetes.Interface, clusterconfig.ClientInterface, *appsv1.Deployment) { + clientsetinterface, _ := NewFakeClientSet(t) + clientset := clientsetinterface.GetKubernetesClient() + clientsetargorollouts := clientsetinterface.GetArgoRolloutsClient() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + deploymentName := "test-deployment2" + + deployment := CreateTestDeployment(testNamespace, deploymentName, 0, AppLabels) + _, err = clientset.AppsV1().Deployments(testNamespace).Create(context.TODO(), deployment, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake deployment: %v", err) + } + var rollout *v1alpha1.Rollout + if implementationType == "bluegreen" { + rollout = CreateTestArgoRolloutWithDeployment(testNamespace, rolloutName, deployment, AppLabels, implementationType) + } else { + rollout = CreateTestArgoRolloutWithDeployment(testNamespace, rolloutName, deployment, AppLabels, implementationType) + } + + _, err = clientsetargorollouts.ArgoprojV1alpha1().Rollouts(testNamespace).Create(context.TODO(), rollout, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake argo rollout: %v", err) + } + + analysisTemplate := CreateTestArgoRolloutAnalysis(testNamespace, analysisName, AppLabels) + _, err = clientsetargorollouts.ArgoprojV1alpha1().AnalysisTemplates(testNamespace).Create(context.TODO(), analysisTemplate, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake analysis template for argo rollout: %v", err) + } + + return clientset, clientsetinterface, deployment + +} + +func createTestArgoRolloutMultiResourcesWithClusterAnalysis(t *testing.T, rolloutName string, analysisName string, implementationType string) (kubernetes.Interface, clusterconfig.ClientInterface, *appsv1.Deployment) { + clientsetinterface, _ := NewFakeClientSet(t) + clientset := clientsetinterface.GetKubernetesClient() + clientsetargorollouts := clientsetinterface.GetArgoRolloutsClient() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + deploymentName := "test-deployment2" + + deployment := CreateTestDeployment(testNamespace, deploymentName, 0, AppLabels) + _, err = clientset.AppsV1().Deployments(testNamespace).Create(context.TODO(), deployment, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake deployment: %v", err) + } + var rollout *v1alpha1.Rollout + if implementationType == "bluegreen" { + rollout = CreateTestArgoRolloutWithDeployment(testNamespace, rolloutName, deployment, AppLabels, implementationType) + } else { + rollout = CreateTestArgoRolloutWithDeployment(testNamespace, rolloutName, deployment, AppLabels, implementationType) + } + + _, err = clientsetargorollouts.ArgoprojV1alpha1().Rollouts(testNamespace).Create(context.TODO(), rollout, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake argo rollout: %v", err) + } + + analysisTemplate := CreateTestArgoRolloutClusterAnalysis(analysisName, AppLabels) + _, err = clientsetargorollouts.ArgoprojV1alpha1().ClusterAnalysisTemplates().Create(context.TODO(), analysisTemplate, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake analysis template for argo rollout: %v", err) + } + + return clientset, clientsetinterface, deployment + +} + +func TestGetUnusedArgoRolloutsStructuredByNamespace(t *testing.T) { + rolloutName := "test-rollout-1" + implementationType := "canary" + clientset, clientsetinterface, deployment := createTestArgoRolloutMultiResources(t, rolloutName, implementationType) + + err := clientset.AppsV1().Deployments(testNamespace).Delete(context.TODO(), deployment.Name, v1.DeleteOptions{}) + if err != nil { + t.Fatalf("Error on delete test deployment %s for argorollout testing: %v", deployment.GetName(), err) + } + + parseOpts := common.Opts{} + parseOpts.GroupBy = "namespace" + + opts := &filters.Options{} + opts.IncludeThirdPartyCrds = append(opts.IncludeThirdPartyCrds, "argo-rollouts") + opts.IncludeNamespaces = append(opts.IncludeNamespaces, "testNamespace") + + resources := make(map[string]map[string][]ResourceInfo) + resources[testNamespace] = make(map[string][]ResourceInfo) + + var outputBuffer bytes.Buffer + var jsonResponse []byte + + GetUnusedCrdsThirdParty("namespace", clientsetinterface, testNamespace, opts, resources, true) + jsonResponse, err = json.MarshalIndent(resources, "", " ") + if err != nil { + t.Fatalf("Error marshaling jsonResponse: %v", err) + } + + unused, err := unusedResourceFormatter("json", outputBuffer, parseOpts, jsonResponse) + if err != nil { + t.Fatalf("Error on get argorollout unused: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "ArgoRollout": { + rolloutName, + }, + }, + } + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(unused), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func TestGetUnusedArgoRolloutsStructuredByResources(t *testing.T) { + rolloutName := "test-rollout-1" + implementationType := "canary" + clientset, clientsetinterface, deployment := createTestArgoRolloutMultiResources(t, rolloutName, implementationType) + + err := clientset.AppsV1().Deployments(testNamespace).Delete(context.TODO(), deployment.Name, v1.DeleteOptions{}) + if err != nil { + t.Fatalf("Error on delete test deployment %s for argorollout testing: %v", deployment.GetName(), err) + } + + parseOpts := common.Opts{} + parseOpts.GroupBy = "resource" + + opts := &filters.Options{} + opts.IncludeThirdPartyCrds = append(opts.IncludeThirdPartyCrds, "argo-rollouts") + opts.IncludeNamespaces = append(opts.IncludeNamespaces, "testNamespace") + + resources := make(map[string]map[string][]ResourceInfo) + resources[testNamespace] = make(map[string][]ResourceInfo) + + var outputBuffer bytes.Buffer + var jsonResponse []byte + + GetUnusedCrdsThirdParty("resource", clientsetinterface, testNamespace, opts, resources, true) + + jsonResponse, err = json.MarshalIndent(resources, "", " ") + if err != nil { + t.Fatalf("Error marshaling jsonResponse: %v", err) + } + + unused, err := unusedResourceFormatter("json", outputBuffer, parseOpts, jsonResponse) + if err != nil { + t.Fatalf("Error on get argorollout unused: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + "ArgoRollout": { + testNamespace: { + rolloutName, + }, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(unused), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func TestGetUnusedArgoRolloutsCanary(t *testing.T) { + rolloutName := "test-rollout-2" + implementationType := "canary" + clientset, clientsetinterface, deployment := createTestArgoRolloutMultiResources(t, rolloutName, implementationType) + + err := clientset.AppsV1().Deployments(testNamespace).Delete(context.TODO(), deployment.GetName(), v1.DeleteOptions{}) + if err != nil { + t.Fatalf("Error on delete test deployment %s for argorollout testing: %v", deployment.GetName(), err) + } + + opts := &filters.Options{} + opts.IncludeThirdPartyCrds = append(opts.IncludeThirdPartyCrds, "argo-rollouts") + opts.IncludeNamespaces = append(opts.IncludeNamespaces, testNamespace) + + resources := GetUnusedArgoRollouts(clientsetinterface, testNamespace, opts) + if err != nil { + t.Fatalf("Error marshaling jsonResponse: %v", err) + } + var argoRolloutsDiffTest []ResourceInfo + argoRolloutsDiffTest = append(argoRolloutsDiffTest, ResourceInfo{Name: rolloutName, Reason: "Rollout has no deployments"}) + expectedOutput := ResourceDiff{ + "ArgoRollouts", + argoRolloutsDiffTest, + } + + if !reflect.DeepEqual(expectedOutput, resources) { + t.Errorf("Expected output does not match actual output") + } +} + +func TestGetUnusedArgoRolloutsBlueGreen(t *testing.T) { + rolloutName := "test-rollout-3" + implementationType := "bluegreen" + clientset, clientsetinterface, deployment := createTestArgoRolloutMultiResources(t, rolloutName, implementationType) + + err := clientset.AppsV1().Deployments(testNamespace).Delete(context.TODO(), deployment.GetName(), v1.DeleteOptions{}) + if err != nil { + t.Fatalf("Error on delete test deployment %s for argorollout testing: %v", deployment.GetName(), err) + } + + opts := &filters.Options{} + opts.IncludeThirdPartyCrds = append(opts.IncludeThirdPartyCrds, "argo-rollouts") + opts.IncludeNamespaces = append(opts.IncludeNamespaces, testNamespace) + + resources := GetUnusedArgoRollouts(clientsetinterface, testNamespace, opts) + if err != nil { + t.Fatalf("Error marshaling jsonResponse: %v", err) + } + var argoRolloutsDiffTest []ResourceInfo + argoRolloutsDiffTest = append(argoRolloutsDiffTest, ResourceInfo{Name: rolloutName, Reason: "Rollout has no deployments"}) + expectedOutput := ResourceDiff{ + "ArgoRollouts", + argoRolloutsDiffTest, + } + + if !reflect.DeepEqual(expectedOutput, resources) { + t.Errorf("Expected output does not match actual output") + } +} + +func TestGetUnusedArgoRolloutsAnalysisTemplatesCanary(t *testing.T) { + analysisName := "test-analysys-template-1" + rolloutName := "test-rollout-4" + implementationType := "canary" + clientset, clientsetinterface, deployment := createTestArgoRolloutMultiResourcesWithAnalysis(t, rolloutName, analysisName, implementationType) + + err := clientset.AppsV1().Deployments(testNamespace).Delete(context.TODO(), deployment.GetName(), v1.DeleteOptions{}) + if err != nil { + t.Fatalf("Error on delete test deployment %s for argorollout testing: %v", deployment.GetName(), err) + } + + opts := &filters.Options{} + opts.IncludeThirdPartyCrds = append(opts.IncludeThirdPartyCrds, "argo-rollouts") + opts.IncludeNamespaces = append(opts.IncludeNamespaces, testNamespace) + + resources := GetUnusedArgoRolloutsAnalysisTemplates(clientsetinterface, testNamespace, opts) + if err != nil { + t.Fatalf("Error marshaling jsonResponse: %v", err) + } + + var argoRolloutsDiffTest []ResourceInfo + argoRolloutsDiffTest = append(argoRolloutsDiffTest, ResourceInfo{Name: analysisName, Reason: "Argo Rollouts Analysis Templates is not in use"}) + expectedOutput := ResourceDiff{ + "Analysis Templates", + argoRolloutsDiffTest, + } + + if !reflect.DeepEqual(expectedOutput, resources) { + t.Errorf("Expected output does not match actual output") + } +} + +func TestGetUnusedArgoRolloutsAnalysisTemplatesBlueGreen(t *testing.T) { + analysisName := "test-analysys-template-2" + rolloutName := "test-rollout-5" + implementationType := "bluegreen" + clientset, clientsetinterface, deployment := createTestArgoRolloutMultiResourcesWithAnalysis(t, rolloutName, analysisName, implementationType) + + err := clientset.AppsV1().Deployments(testNamespace).Delete(context.TODO(), deployment.GetName(), v1.DeleteOptions{}) + if err != nil { + t.Fatalf("Error on delete test deployment %s for argorollout testing: %v", deployment.GetName(), err) + } + + opts := &filters.Options{} + opts.IncludeThirdPartyCrds = append(opts.IncludeThirdPartyCrds, "argo-rollouts") + opts.IncludeNamespaces = append(opts.IncludeNamespaces, testNamespace) + + resources := GetUnusedArgoRolloutsAnalysisTemplates(clientsetinterface, testNamespace, opts) + if err != nil { + t.Fatalf("Error marshaling jsonResponse: %v", err) + } + + var argoRolloutsDiffTest []ResourceInfo + argoRolloutsDiffTest = append(argoRolloutsDiffTest, ResourceInfo{Name: analysisName, Reason: "Argo Rollouts Analysis Templates is not in use"}) + expectedOutput := ResourceDiff{ + "Analysis Templates", + argoRolloutsDiffTest, + } + + if !reflect.DeepEqual(expectedOutput, resources) { + t.Errorf("Expected output does not match actual output") + } +} + +func TestGetUnusedArgoRolloutsClusterAnalysisTemplatesCanary(t *testing.T) { + analysisName := "test-analysys-template-3" + rolloutName := "test-rollout-6" + implementationType := "canary" + clientset, clientsetinterface, deployment := createTestArgoRolloutMultiResourcesWithClusterAnalysis(t, rolloutName, analysisName, implementationType) + + err := clientset.AppsV1().Deployments(testNamespace).Delete(context.TODO(), deployment.GetName(), v1.DeleteOptions{}) + if err != nil { + t.Fatalf("Error on delete test deployment %s for argorollout testing: %v", deployment.GetName(), err) + } + + opts := &filters.Options{} + opts.IncludeThirdPartyCrds = append(opts.IncludeThirdPartyCrds, "argo-rollouts") + opts.IncludeNamespaces = append(opts.IncludeNamespaces, testNamespace) + + resources := GetUnusedArgoRolloutsClusterAnalysisTemplates(clientsetinterface, testNamespace, opts) + if err != nil { + t.Fatalf("Error marshaling jsonResponse: %v", err) + } + + var argoRolloutsDiffTest []ResourceInfo + argoRolloutsDiffTest = append(argoRolloutsDiffTest, ResourceInfo{Name: analysisName, Reason: "Argo Rollouts Cluster Analysis Templates is not in use"}) + expectedOutput := ResourceDiff{ + "Cluster Analysis Templates", + argoRolloutsDiffTest, + } + + if !reflect.DeepEqual(expectedOutput, resources) { + t.Errorf("Expected output does not match actual output") + } +} + +func TestGetUnusedArgoRolloutsClusterAnalysisTemplatesBlueGreen(t *testing.T) { + analysisName := "test-analysys-template-4" + rolloutName := "test-rollout-7" + implementationType := "bluegreen" + clientset, clientsetinterface, deployment := createTestArgoRolloutMultiResourcesWithClusterAnalysis(t, rolloutName, analysisName, implementationType) + + err := clientset.AppsV1().Deployments(testNamespace).Delete(context.TODO(), deployment.GetName(), v1.DeleteOptions{}) + if err != nil { + t.Fatalf("Error on delete test deployment %s for argorollout testing: %v", deployment.GetName(), err) + } + + opts := &filters.Options{} + opts.IncludeThirdPartyCrds = append(opts.IncludeThirdPartyCrds, "argo-rollouts") + opts.IncludeNamespaces = append(opts.IncludeNamespaces, testNamespace) + + resources := GetUnusedArgoRolloutsClusterAnalysisTemplates(clientsetinterface, testNamespace, opts) + if err != nil { + t.Fatalf("Error marshaling jsonResponse: %v", err) + } + + var argoRolloutsDiffTest []ResourceInfo + argoRolloutsDiffTest = append(argoRolloutsDiffTest, ResourceInfo{Name: analysisName, Reason: "Argo Rollouts Cluster Analysis Templates is not in use"}) + expectedOutput := ResourceDiff{ + "Cluster Analysis Templates", + argoRolloutsDiffTest, + } + + if !reflect.DeepEqual(expectedOutput, resources) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/config_test.go b/pkg/kor/config_test.go new file mode 100644 index 00000000..e997d7d1 --- /dev/null +++ b/pkg/kor/config_test.go @@ -0,0 +1,57 @@ +package kor + +import ( + "testing" + + "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + fakeargorollouts "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + + "github.com/yonahd/kor/pkg/clusterconfig" +) + +type FakeClientSet struct { + coreClient *fake.Clientset + coreClientArgoRollouts *fakeargorollouts.Clientset +} + +// GetArgoRolloutsClient implements ClientInterface. +func (c *FakeClientSet) GetArgoRolloutsClient() versioned.Interface { + return c.coreClientArgoRollouts +} + +// GetKubernetesClient implements ClientInterface. +func (c *FakeClientSet) GetKubernetesClient() kubernetes.Interface { + return c.coreClient +} + +func NewFakeClientSet(t *testing.T) (clusterconfig.ClientInterface, error) { + coreClient := fake.NewSimpleClientset() + coreClientArgoRollouts := fakeargorollouts.NewSimpleClientset() + + // Return the ClientSet struct + return &FakeClientSet{ + coreClient: coreClient, + coreClientArgoRollouts: coreClientArgoRollouts, + }, nil +} + +func GetFakeKubeClient(t *testing.T) (clusterconfig.ClientInterface, error) { + clientsetinterface, err := NewFakeClientSet(t) + + if err != nil { + t.Fatalf("Error creating fake clientset. Error: %v", err) + } + + return clientsetinterface, nil +} + +func SetConfigsForTests(t *testing.T) clusterconfig.ClientInterface { + clientsetinterface, err := GetFakeKubeClient(t) + if err != nil { + t.Fatalf("Error on setting config: %v", err) + } + + return clientsetinterface +} diff --git a/pkg/kor/create_test_resources.go b/pkg/kor/create_test_resources.go index 8e2c2ce6..3d9ab4f5 100644 --- a/pkg/kor/create_test_resources.go +++ b/pkg/kor/create_test_resources.go @@ -1,6 +1,7 @@ package kor import ( + argorollouts "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" @@ -431,3 +432,59 @@ func CreateTestNetworkPolicy(name, namespace string, labels map[string]string, p }, } } + +func CreateTestArgoRolloutWithDeployment(namespace, name string, deplomentWorkLoadRef *appsv1.Deployment, labels map[string]string, implementationType string) *argorollouts.Rollout { + rollout := &argorollouts.Rollout{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + Labels: labels, + }, + } + if implementationType == "canary" { + rollout.Spec = argorollouts.RolloutSpec{ + WorkloadRef: &argorollouts.ObjectRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: deplomentWorkLoadRef.GetName(), + }, + Strategy: argorollouts.RolloutStrategy{ + Canary: &argorollouts.CanaryStrategy{}, + }, + } + } + if implementationType == "bluegreen" { + rollout.Spec = argorollouts.RolloutSpec{ + WorkloadRef: &argorollouts.ObjectRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: deplomentWorkLoadRef.GetName(), + }, + Strategy: argorollouts.RolloutStrategy{ + BlueGreen: &argorollouts.BlueGreenStrategy{}, + }, + } + } + return rollout +} + +func CreateTestArgoRolloutAnalysis(namespace, analysisName string, labels map[string]string) *argorollouts.AnalysisTemplate { + + return &argorollouts.AnalysisTemplate{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: analysisName, + Labels: labels, + }, + } +} + +func CreateTestArgoRolloutClusterAnalysis(analysisName string, labels map[string]string) *argorollouts.ClusterAnalysisTemplate { + return &argorollouts.ClusterAnalysisTemplate{ + ObjectMeta: v1.ObjectMeta{ + + Name: analysisName, + Labels: labels, + }, + } +} diff --git a/pkg/kor/exporter.go b/pkg/kor/exporter.go index 39b66c3b..45891df8 100644 --- a/pkg/kor/exporter.go +++ b/pkg/kor/exporter.go @@ -15,6 +15,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" ) @@ -34,16 +35,16 @@ func init() { } // TODO: add option to change port / url !? -func Exporter(filterOptions *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts, resourceList []string) { +func Exporter(filterOptions *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, clientsetinterface clusterconfig.ClientInterface, outputFormat string, opts common.Opts, resourceList []string) { http.Handle("/metrics", promhttp.Handler()) fmt.Println("Server listening on :8080") - go exportMetrics(filterOptions, clientset, apiExtClient, dynamicClient, outputFormat, opts, resourceList) // Start exporting metrics in the background + go exportMetrics(filterOptions, clientset, apiExtClient, dynamicClient, clientsetinterface, outputFormat, opts, resourceList) // Start exporting metrics in the background if err := http.ListenAndServe(":8080", nil); err != nil { fmt.Println(err) } } -func exportMetrics(filterOptions *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts, resourceList []string) { +func exportMetrics(filterOptions *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, clientsetinterface clusterconfig.ClientInterface, outputFormat string, opts common.Opts, resourceList []string) { exporterInterval := os.Getenv("EXPORTER_INTERVAL") if exporterInterval == "" { exporterInterval = "10" @@ -56,7 +57,7 @@ func exportMetrics(filterOptions *filters.Options, clientset kubernetes.Interfac for { fmt.Println("collecting unused resources") - if korOutput, err := getUnusedResources(filterOptions, clientset, apiExtClient, dynamicClient, outputFormat, opts, resourceList); err != nil { + if korOutput, err := getUnusedResources(filterOptions, clientset, apiExtClient, dynamicClient, clientsetinterface, outputFormat, opts, resourceList); err != nil { fmt.Println(err) os.Exit(1) } else { @@ -80,10 +81,10 @@ func exportMetrics(filterOptions *filters.Options, clientset kubernetes.Interfac } } -func getUnusedResources(filterOptions *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts, resourceList []string) (string, error) { +func getUnusedResources(filterOptions *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, clientsetinterface clusterconfig.ClientInterface, outputFormat string, opts common.Opts, resourceList []string) (string, error) { if len(resourceList) == 0 || (len(resourceList) == 1 && resourceList[0] == "all") { - return GetUnusedAll(filterOptions, clientset, apiExtClient, dynamicClient, outputFormat, opts) + return GetUnusedAll(filterOptions, clientset, apiExtClient, dynamicClient, clientsetinterface, outputFormat, opts) } - return GetUnusedMulti(strings.Join(resourceList, ","), filterOptions, clientset, apiExtClient, dynamicClient, outputFormat, opts) + return GetUnusedMulti(strings.Join(resourceList, ","), filterOptions, clientset, apiExtClient, dynamicClient, clientsetinterface, outputFormat, opts) } diff --git a/pkg/kor/formatter.go b/pkg/kor/formatter.go index cec7a0c5..932f04d0 100644 --- a/pkg/kor/formatter.go +++ b/pkg/kor/formatter.go @@ -235,3 +235,12 @@ func FormatOutputAll(namespace string, allDiffs []ResourceDiff, opts common.Opts table.Render() return fmt.Sprintf("Unused resources in namespace: %q\n%s\n", namespace, buf.String()) } + +func SkipIfContainsValue(data []ResourceInfo, key string, value interface{}) bool { + for _, item := range data { + if item.Name == value { + return true + } + } + return false +} diff --git a/pkg/kor/kor.go b/pkg/kor/kor.go index be6968c0..2f5b135d 100644 --- a/pkg/kor/kor.go +++ b/pkg/kor/kor.go @@ -2,18 +2,8 @@ package kor import ( "encoding/json" - "fmt" - "os" - "path/filepath" "regexp" "sort" - - apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" ) type ExceptionResource struct { @@ -55,72 +45,6 @@ func RemoveDuplicatesAndSort(slice []string) []string { return uniqueSlice } -func GetKubeConfigPath() string { - home := homedir.HomeDir() - return filepath.Join(home, ".kube", "config") -} - -func GetConfig(kubeconfig string) (*rest.Config, error) { - if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token"); err == nil { - return rest.InClusterConfig() - } - - if kubeconfig == "" { - if configEnv := os.Getenv("KUBECONFIG"); configEnv != "" { - kubeconfig = configEnv - } else { - kubeconfig = GetKubeConfigPath() - } - } - - return clientcmd.BuildConfigFromFlags("", kubeconfig) -} - -func GetKubeClient(kubeconfig string) *kubernetes.Clientset { - config, err := GetConfig(kubeconfig) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err) - os.Exit(1) - } - - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err) - os.Exit(1) - } - return clientset -} - -func GetAPIExtensionsClient(kubeconfig string) *apiextensionsclientset.Clientset { - config, err := GetConfig(kubeconfig) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err) - os.Exit(1) - } - - clientset, err := apiextensionsclientset.NewForConfig(config) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err) - os.Exit(1) - } - return clientset -} - -func GetDynamicClient(kubeconfig string) *dynamic.DynamicClient { - config, err := GetConfig(kubeconfig) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err) - os.Exit(1) - } - - clientset, err := dynamic.NewForConfig(config) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err) - os.Exit(1) - } - return clientset -} - // TODO create formatter by resource "#", "Resource Name", "Namespace" // TODO Functions that use this object are accompanied by repeated data acquisition operations and can be optimized. func CalculateResourceDifference(usedResourceNames []string, allResourceNames []string) []string { diff --git a/pkg/kor/kor_test.go b/pkg/kor/kor_test.go index eafddb17..03622283 100644 --- a/pkg/kor/kor_test.go +++ b/pkg/kor/kor_test.go @@ -4,6 +4,8 @@ import ( "os" "sort" "testing" + + "github.com/yonahd/kor/pkg/clusterconfig" ) func stringSlicesEqual(a, b []string) bool { @@ -99,7 +101,7 @@ func TestGetKubeClientFromEnvVar(t *testing.T) { defer os.Setenv("KUBECONFIG", originalKCEnv) os.Setenv("KUBECONFIG", configFile.Name()) - kcs := GetKubeClient("") + kcs := clusterconfig.GetKubeClient("") if kcs == nil { t.Errorf("Expected valid clientSet") } @@ -125,7 +127,7 @@ func TestGetKubeClientFromInput(t *testing.T) { os.Setenv("KUBERNETES_SERVICE_PORT", oldKubeServicePort) }() - kcs := GetKubeClient(configFile.Name()) + kcs := clusterconfig.GetKubeClient(configFile.Name()) if kcs == nil { t.Errorf("Expected valid clientSet") } diff --git a/pkg/kor/multi.go b/pkg/kor/multi.go index ef2dfd94..323f2c41 100644 --- a/pkg/kor/multi.go +++ b/pkg/kor/multi.go @@ -11,6 +11,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" ) @@ -99,7 +100,7 @@ func retrieveNamespaceDiffs(clientset kubernetes.Interface, namespace string, re return allDiffs } -func GetUnusedMulti(resourceNames string, filterOpts *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { +func GetUnusedMulti(resourceNames string, filterOpts *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, clientsetinterface clusterconfig.ClientInterface, outputFormat string, opts common.Opts) (string, error) { resourceList := strings.Split(resourceNames, ",") namespaces := filterOpts.Namespaces(clientset) resources := make(map[string]map[string][]ResourceInfo) diff --git a/pkg/kor/multi_test.go b/pkg/kor/multi_test.go index 3c4828f2..d617cf11 100644 --- a/pkg/kor/multi_test.go +++ b/pkg/kor/multi_test.go @@ -8,14 +8,16 @@ import ( corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes" + "github.com/yonahd/kor/pkg/clusterconfig" "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" ) -func createTestMultiResources(t *testing.T) *fake.Clientset { - clientset := fake.NewClientset() +func createTestMultiResources(t *testing.T) (kubernetes.Interface, clusterconfig.ClientInterface) { + clientsetinterface, _ := NewFakeClientSet(t) + clientset := clientsetinterface.GetKubernetesClient() _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ ObjectMeta: v1.ObjectMeta{Name: testNamespace}, @@ -37,12 +39,12 @@ func createTestMultiResources(t *testing.T) *fake.Clientset { t.Fatalf("Error creating fake configmap: %v", err) } - return clientset + return clientset, clientsetinterface } func TestRetrieveNamespaceDiff(t *testing.T) { - clientset := createTestMultiResources(t) + clientset, _ := createTestMultiResources(t) resourceList := []string{"cm", "pdb", "deployment"} filterOpts := &filters.Options{} @@ -67,7 +69,7 @@ func TestRetrieveNamespaceDiff(t *testing.T) { } func TestGetUnusedMulti(t *testing.T) { - clientset := createTestMultiResources(t) + clientset, clientsetinterface := createTestMultiResources(t) resourceList := "cm,pdb,deployment" opts := common.Opts{ @@ -79,7 +81,7 @@ func TestGetUnusedMulti(t *testing.T) { GroupBy: "namespace", } - output, err := GetUnusedMulti(resourceList, &filters.Options{}, clientset, nil, nil, "json", opts) + output, err := GetUnusedMulti(resourceList, &filters.Options{}, clientset, nil, nil, clientsetinterface, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedMulti: %v", err)