diff --git a/README.md b/README.md index 3f117ef4..a9955a39 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,8 @@ kor [subcommand] --help | Resource | What it looks for | Known False Positives ⚠️ | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ConfigMaps | ConfigMaps not used in the following places:
- Pods
- Containers
- ConfigMaps used through Volumes
- ConfigMaps used through environment variables | ConfigMaps used by resources which don't explicitly state them in the config.
e.g Grafana dashboards loaded dynamically OPA policies fluentd configs CRD configs | -| Secrets | Secrets not used in the following places:
- Pods
- Containers
- Secrets used through volumes
- Secrets used through environment variables
- Secrets used by Ingress TLS
- Secrets used by ServiceAccounts | Secrets used by resources which don't explicitly state them in the config e.g. secrets used by CRDs | +| ConfigMaps | ConfigMaps not used in the following places:
- Pods
- Containers
- ConfigMaps used through Volumes
- ConfigMaps used through environment variables
- **Argo WorkflowTemplates** (when CRD is present) | ConfigMaps used by resources which don't explicitly state them in the config.
e.g Grafana dashboards loaded dynamically OPA policies fluentd configs CRD configs (excluding Argo WorkflowTemplates) | +| Secrets | Secrets not used in the following places:
- Pods
- Containers
- Secrets used through volumes
- Secrets used through environment variables
- Secrets used by Ingress TLS
- Secrets used by ServiceAccounts
- **Argo WorkflowTemplates** (when CRD is present) | Secrets used by resources which don't explicitly state them in the config e.g. secrets used by CRDs (excluding Argo WorkflowTemplates) | | Services | Services with no endpoints | | | Deployments | Deployments with no replicas | | | ServiceAccounts | ServiceAccounts unused by Pods
ServiceAccounts unused by RoleBinding or ClusterRoleBinding | | @@ -189,7 +189,7 @@ kor [subcommand] --help | ClusterRoles | ClusterRoles not used in RoleBinding or ClusterRoleBinding
ClusterRoles not used in ClusterRole aggregation | | | RoleBindings | RoleBindings referencing invalid Role, ClusterRole, or ServiceAccounts | | | ClusterRoleBindings | ClusterRoleBindings referencing invalid ClusterRole or ServiceAccounts | | -| PVCs | PVCs not used in Pods | | +| PVCs | PVCs not used in the following places:
- Pods
- **Argo WorkflowTemplates** (when CRD is present) | | | Ingresses | Ingresses not pointing at any Service | | | HPAs | HPAs not used in Deployments
HPAs not used in StatefulSets | | | CRDs | CRDs not used the cluster | | @@ -202,6 +202,50 @@ kor [subcommand] --help | NetworkPolicies | NetworkPolicies with no Pods selected by podSelector or Ingress / Egress rules | | VolumeAttachments | VolumeAttachments referencing a non-existent Node, PV, or CSIDriver | +## Argo Workflows Integration + +Kor automatically detects and scans **Argo Workflows WorkflowTemplate** CRDs to prevent false positives for ConfigMaps, Secrets, and PVCs referenced by WorkflowTemplates. + +### Key Features + +- **Automatic**: Detects Argo Workflows installation, works with all existing commands +- **Zero Configuration**: No setup required, activates only when WorkflowTemplate CRD exists +- **Comprehensive**: Scans environment variables, envFrom, volumes, projected volumes, and synchronization semaphores + +### Supported Reference Patterns + +Kor scans WorkflowTemplates for the following resource references: + +**ConfigMaps:** +- Environment variables: `env[].valueFrom.configMapKeyRef` +- Environment from: `envFrom[].configMapRef` +- Volumes: `volumes[].configMap` +- Projected volumes: `volumes[].projected.sources[].configMap` +- Synchronization semaphores: `spec.synchronization.semaphore.configMapKeyRef` + +**Secrets:** +- Environment variables: `env[].valueFrom.secretKeyRef` +- Environment from: `envFrom[].secretRef` +- Volumes: `volumes[].secret` +- Projected volumes: `volumes[].projected.sources[].secret` + +**PVCs:** +- Volumes: `volumes[].persistentVolumeClaim` + +### Before vs After + +**Before (False Positives):** +```bash +$ kor configmap --include-namespaces production +Unused ConfigMaps: workflow-config, app-settings +``` + +**After (Accurate):** +```bash +$ kor configmap --include-namespaces production +No unused ConfigMaps found +``` + ### Deleting Unused resources If you want to delete resources in an interactive way using Kor you can run: diff --git a/cmd/kor/configmaps.go b/cmd/kor/configmaps.go index 161b2a61..7d713978 100644 --- a/cmd/kor/configmaps.go +++ b/cmd/kor/configmaps.go @@ -16,7 +16,8 @@ var configmapCmd = &cobra.Command{ Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) - if response, err := kor.GetUnusedConfigmaps(filterOptions, clientset, outputFormat, opts); err != nil { + dynamicClient := kor.GetDynamicClient(kubeconfig) + if response, err := kor.GetUnusedConfigmaps(filterOptions, clientset, dynamicClient, outputFormat, opts); err != nil { fmt.Println(err) } else { utils.PrintLogo(outputFormat) diff --git a/cmd/kor/pvc.go b/cmd/kor/pvc.go index e29f9c61..28fccbb5 100644 --- a/cmd/kor/pvc.go +++ b/cmd/kor/pvc.go @@ -16,8 +16,9 @@ var pvcCmd = &cobra.Command{ Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) + dynamicClient := kor.GetDynamicClient(kubeconfig) - if response, err := kor.GetUnusedPvcs(filterOptions, clientset, outputFormat, opts); err != nil { + if response, err := kor.GetUnusedPvcs(filterOptions, clientset, dynamicClient, outputFormat, opts); err != nil { fmt.Println(err) } else { utils.PrintLogo(outputFormat) diff --git a/cmd/kor/secrets.go b/cmd/kor/secrets.go index b484d4af..0126b4b4 100644 --- a/cmd/kor/secrets.go +++ b/cmd/kor/secrets.go @@ -16,8 +16,9 @@ var secretCmd = &cobra.Command{ Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) + dynamicClient := kor.GetDynamicClient(kubeconfig) - if response, err := kor.GetUnusedSecrets(filterOptions, clientset, outputFormat, opts); err != nil { + if response, err := kor.GetUnusedSecrets(filterOptions, clientset, dynamicClient, outputFormat, opts); err != nil { fmt.Println(err) } else { utils.PrintLogo(outputFormat) diff --git a/go.mod b/go.mod index 14b77b90..0ecc60f3 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,13 @@ module github.com/yonahd/kor -go 1.24.0 +go 1.24.2 + +toolchain go1.24.6 require ( + github.com/argoproj/argo-workflows/v3 v3.7.1 github.com/fatih/color v1.18.0 - github.com/olekukonko/tablewriter v0.0.5 + github.com/olekukonko/tablewriter v1.0.7 github.com/prometheus/client_golang v1.23.0 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 @@ -12,7 +15,7 @@ require ( k8s.io/apiextensions-apiserver v0.33.3 k8s.io/apimachinery v0.33.3 k8s.io/client-go v0.33.3 - k8s.io/utils v0.0.0-20241210054802-24370beab758 + k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 sigs.k8s.io/yaml v1.6.0 ) @@ -20,38 +23,41 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-viper/mapstructure/v2 v2.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.9.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/cast v1.9.2 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -62,14 +68,20 @@ require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.9.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect ) + +replace github.com/olekukonko/tablewriter => github.com/olekukonko/tablewriter v0.0.5 diff --git a/go.sum b/go.sum index 75b96b4f..a144c3e5 100644 --- a/go.sum +++ b/go.sum @@ -1,49 +1,72 @@ +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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/argoproj/argo-workflows/v3 v3.7.1 h1:8xynztGc+tUuUq/SB9dHGp5EDI6UkSmuJeQPhHQrbek= +github.com/argoproj/argo-workflows/v3 v3.7.1/go.mod h1:UleRW8WI3LHU/saPk9jEagm8zJFXWAw90iDVIZDA+UA= 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -54,25 +77,21 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -82,18 +101,21 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= @@ -101,19 +123,22 @@ github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGI github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -121,14 +146,10 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -148,23 +169,40 @@ go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -174,9 +212,13 @@ 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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -186,6 +228,24 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -195,9 +255,12 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= @@ -210,15 +273,15 @@ 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-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= -k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/kor/all.go b/pkg/kor/all.go index ab1ddbae..a8b3937e 100644 --- a/pkg/kor/all.go +++ b/pkg/kor/all.go @@ -26,8 +26,8 @@ type ResourceDiff struct { diff []ResourceInfo } -func getUnusedCMs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ResourceDiff { - cmDiff, err := processNamespaceCM(clientset, namespace, filterOpts, opts) +func getUnusedCMs(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ResourceDiff { + cmDiff, err := processNamespaceCM(clientset, dynamicClient, namespace, filterOpts, opts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "configmaps", namespace, err) } @@ -50,8 +50,8 @@ func getUnusedSVCs(clientset kubernetes.Interface, namespace string, filterOpts return namespaceSVCDiff } -func getUnusedSecrets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ResourceDiff { - secretDiff, err := processNamespaceSecret(clientset, namespace, filterOpts, opts) +func getUnusedSecrets(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ResourceDiff { + secretDiff, err := processNamespaceSecret(clientset, dynamicClient, namespace, filterOpts, opts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "secrets", namespace, err) } @@ -146,8 +146,8 @@ func getUnusedHpas(clientset kubernetes.Interface, namespace string, filterOpts return namespaceHpaDiff } -func getUnusedPvcs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ResourceDiff { - pvcDiff, err := processNamespacePvcs(clientset, namespace, filterOpts, opts) +func getUnusedPvcs(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ResourceDiff { + pvcDiff, err := processNamespacePvcs(clientset, dynamicClient, namespace, filterOpts, opts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pvcs", namespace, err) } @@ -302,21 +302,21 @@ 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, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { resources := make(map[string]map[string][]ResourceInfo) for _, namespace := range filterOpts.Namespaces(clientset) { switch opts.GroupBy { case "namespace": resources[namespace] = make(map[string][]ResourceInfo) - resources[namespace]["ConfigMap"] = getUnusedCMs(clientset, namespace, filterOpts, opts).diff + resources[namespace]["ConfigMap"] = getUnusedCMs(clientset, dynamicClient, namespace, filterOpts, opts).diff resources[namespace]["Service"] = getUnusedSVCs(clientset, namespace, filterOpts, opts).diff - resources[namespace]["Secret"] = getUnusedSecrets(clientset, namespace, filterOpts, opts).diff + resources[namespace]["Secret"] = getUnusedSecrets(clientset, dynamicClient, namespace, filterOpts, opts).diff resources[namespace]["ServiceAccount"] = getUnusedServiceAccounts(clientset, namespace, filterOpts, opts).diff resources[namespace]["Deployment"] = getUnusedDeployments(clientset, namespace, filterOpts, opts).diff resources[namespace]["StatefulSet"] = getUnusedStatefulSets(clientset, namespace, filterOpts, opts).diff resources[namespace]["Role"] = getUnusedRoles(clientset, namespace, filterOpts, opts).diff resources[namespace]["Hpa"] = getUnusedHpas(clientset, namespace, filterOpts, opts).diff - resources[namespace]["Pvc"] = getUnusedPvcs(clientset, namespace, filterOpts, opts).diff + resources[namespace]["Pvc"] = getUnusedPvcs(clientset, dynamicClient, namespace, filterOpts, opts).diff resources[namespace]["Pod"] = getUnusedPods(clientset, namespace, filterOpts, opts).diff resources[namespace]["Ingress"] = getUnusedIngresses(clientset, namespace, filterOpts, opts).diff resources[namespace]["Pdb"] = getUnusedPdbs(clientset, namespace, filterOpts, opts).diff @@ -326,15 +326,15 @@ func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.In resources[namespace]["NetworkPolicy"] = getUnusedNetworkPolicies(clientset, namespace, filterOpts, opts).diff resources[namespace]["RoleBinding"] = getUnusedRoleBindings(clientset, namespace, filterOpts, opts).diff case "resource": - appendResources(resources, "ConfigMap", namespace, getUnusedCMs(clientset, namespace, filterOpts, opts).diff) + appendResources(resources, "ConfigMap", namespace, getUnusedCMs(clientset, dynamicClient, namespace, filterOpts, opts).diff) appendResources(resources, "Service", namespace, getUnusedSVCs(clientset, namespace, filterOpts, opts).diff) - appendResources(resources, "Secret", namespace, getUnusedSecrets(clientset, namespace, filterOpts, opts).diff) + appendResources(resources, "Secret", namespace, getUnusedSecrets(clientset, dynamicClient, namespace, filterOpts, opts).diff) appendResources(resources, "ServiceAccount", namespace, getUnusedServiceAccounts(clientset, namespace, filterOpts, opts).diff) appendResources(resources, "Deployment", namespace, getUnusedDeployments(clientset, namespace, filterOpts, opts).diff) appendResources(resources, "StatefulSet", namespace, getUnusedStatefulSets(clientset, namespace, filterOpts, opts).diff) appendResources(resources, "Role", namespace, getUnusedRoles(clientset, namespace, filterOpts, opts).diff) appendResources(resources, "Hpa", namespace, getUnusedHpas(clientset, namespace, filterOpts, opts).diff) - appendResources(resources, "Pvc", namespace, getUnusedPvcs(clientset, namespace, filterOpts, opts).diff) + appendResources(resources, "Pvc", namespace, getUnusedPvcs(clientset, dynamicClient, namespace, filterOpts, opts).diff) appendResources(resources, "Pod", namespace, getUnusedPods(clientset, namespace, filterOpts, opts).diff) appendResources(resources, "Ingress", namespace, getUnusedIngresses(clientset, namespace, filterOpts, opts).diff) appendResources(resources, "Pdb", namespace, getUnusedPdbs(clientset, namespace, filterOpts, opts).diff) @@ -410,12 +410,12 @@ func GetUnusedAllNonNamespaced(filterOpts *filters.Options, clientset kubernetes func GetUnusedAll(filterOpts *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { if NamespacedFlagUsed { if opts.Namespaced { - return GetUnusedAllNamespaced(filterOpts, clientset, outputFormat, opts) + return GetUnusedAllNamespaced(filterOpts, clientset, dynamicClient, outputFormat, opts) } return GetUnusedAllNonNamespaced(filterOpts, clientset, apiExtClient, dynamicClient, outputFormat, opts) } - unusedAllNamespaced, err := GetUnusedAllNamespaced(filterOpts, clientset, outputFormat, opts) + unusedAllNamespaced, err := GetUnusedAllNamespaced(filterOpts, clientset, dynamicClient, outputFormat, opts) if err != nil { fmt.Printf("err: %v\n", err) } diff --git a/pkg/kor/argo_workflow_validation.go b/pkg/kor/argo_workflow_validation.go new file mode 100644 index 00000000..a0021d03 --- /dev/null +++ b/pkg/kor/argo_workflow_validation.go @@ -0,0 +1,180 @@ +package kor + +import ( + "context" + "sync" + + wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +var ( + // Global state to track if Argo WorkflowTemplate CRD is available + argoWorkflowTemplateEnabled bool + argoWorkflowTemplateOnce sync.Once + workflowTemplateGVR = schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "workflowtemplates", + } +) + +// ArgoWorkflowTemplateResourceReferences holds references to resources used by WorkflowTemplates +type ArgoWorkflowTemplateResourceReferences struct { + ConfigMaps []string + Secrets []string + PVCs []string +} + +// ValidateArgoWorkflowTemplateAvailability checks once if the WorkflowTemplate CRD exists in the cluster +func ValidateArgoWorkflowTemplateAvailability(dynamicClient dynamic.Interface) bool { + argoWorkflowTemplateOnce.Do(func() { + // Try to list WorkflowTemplates to check if the CRD exists + // Use limit=1 to minimize overhead - we just need to know if it exists + _, err := dynamicClient.Resource(workflowTemplateGVR).Namespace("").List(context.TODO(), metav1.ListOptions{Limit: 1}) + argoWorkflowTemplateEnabled = (err == nil) + }) + return argoWorkflowTemplateEnabled +} + +// ValidateResourceReferencesFromArgoWorkflowTemplates scans WorkflowTemplates for resource references +func ValidateResourceReferencesFromArgoWorkflowTemplates(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string) (*ArgoWorkflowTemplateResourceReferences, error) { + refs := &ArgoWorkflowTemplateResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + // If WorkflowTemplate CRD is not available, return empty references + if !ValidateArgoWorkflowTemplateAvailability(dynamicClient) { + return refs, nil + } + + // List all WorkflowTemplates in the namespace + workflowTemplateList, err := dynamicClient.Resource(workflowTemplateGVR).Namespace(namespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + + // Process each WorkflowTemplate + for _, item := range workflowTemplateList.Items { + // Convert unstructured to typed WorkflowTemplate + var workflowTemplate wfv1.WorkflowTemplate + if err := convertUnstructuredToWorkflowTemplate(&item, &workflowTemplate); err != nil { + // Skip invalid WorkflowTemplates but continue processing others + continue + } + + extractResourceReferencesFromTypedWorkflowTemplate(&workflowTemplate, refs) + } + + // Remove duplicates and sort + refs.ConfigMaps = RemoveDuplicatesAndSort(refs.ConfigMaps) + refs.Secrets = RemoveDuplicatesAndSort(refs.Secrets) + refs.PVCs = RemoveDuplicatesAndSort(refs.PVCs) + + return refs, nil +} + +// convertUnstructuredToWorkflowTemplate converts unstructured data to a typed WorkflowTemplate +func convertUnstructuredToWorkflowTemplate(unstructuredItem *unstructured.Unstructured, workflowTemplate *wfv1.WorkflowTemplate) error { + // Convert unstructured to typed WorkflowTemplate using runtime converter + return runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredItem.Object, workflowTemplate) +} + +// extractResourceReferencesFromTypedWorkflowTemplate extracts resource references from a typed WorkflowTemplate +func extractResourceReferencesFromTypedWorkflowTemplate(wt *wfv1.WorkflowTemplate, refs *ArgoWorkflowTemplateResourceReferences) { + // Extract from synchronization semaphore + if wt.Spec.Synchronization != nil && wt.Spec.Synchronization.Semaphore != nil { + if wt.Spec.Synchronization.Semaphore.ConfigMapKeyRef != nil { + refs.ConfigMaps = append(refs.ConfigMaps, wt.Spec.Synchronization.Semaphore.ConfigMapKeyRef.Name) + } + } + + // Extract from templates + for _, template := range wt.Spec.Templates { + extractResourceReferencesFromTemplate(&template, refs) + } + + // Extract from global volumes + extractResourceReferencesFromVolumes(wt.Spec.Volumes, refs) +} + +// extractResourceReferencesFromTemplate extracts resource references from a single template +func extractResourceReferencesFromTemplate(template *wfv1.Template, refs *ArgoWorkflowTemplateResourceReferences) { + // Extract from container environment variables + if template.Container != nil { + extractResourceReferencesFromContainer(template.Container, refs) + } + + // Extract from script environment variables + if template.Script != nil { + extractResourceReferencesFromContainer(&template.Script.Container, refs) + } + + // Extract from volumes + extractResourceReferencesFromVolumes(template.Volumes, refs) +} + +// extractResourceReferencesFromContainer extracts resource references from container environment variables +func extractResourceReferencesFromContainer(container *corev1.Container, refs *ArgoWorkflowTemplateResourceReferences) { + // Extract from environment variables + for _, env := range container.Env { + if env.ValueFrom != nil { + if env.ValueFrom.ConfigMapKeyRef != nil { + refs.ConfigMaps = append(refs.ConfigMaps, env.ValueFrom.ConfigMapKeyRef.Name) + } + if env.ValueFrom.SecretKeyRef != nil { + refs.Secrets = append(refs.Secrets, env.ValueFrom.SecretKeyRef.Name) + } + } + } + + // Extract from envFrom + for _, envFrom := range container.EnvFrom { + if envFrom.ConfigMapRef != nil { + refs.ConfigMaps = append(refs.ConfigMaps, envFrom.ConfigMapRef.Name) + } + if envFrom.SecretRef != nil { + refs.Secrets = append(refs.Secrets, envFrom.SecretRef.Name) + } + } +} + +// extractResourceReferencesFromVolumes extracts resource references from volumes +func extractResourceReferencesFromVolumes(volumes []corev1.Volume, refs *ArgoWorkflowTemplateResourceReferences) { + for _, volume := range volumes { + // ConfigMap volumes + if volume.ConfigMap != nil { + refs.ConfigMaps = append(refs.ConfigMaps, volume.ConfigMap.Name) + } + + // Secret volumes + if volume.Secret != nil { + refs.Secrets = append(refs.Secrets, volume.Secret.SecretName) + } + + // PVC volumes + if volume.PersistentVolumeClaim != nil { + refs.PVCs = append(refs.PVCs, volume.PersistentVolumeClaim.ClaimName) + } + + // Projected volumes + if volume.Projected != nil { + for _, source := range volume.Projected.Sources { + if source.ConfigMap != nil { + refs.ConfigMaps = append(refs.ConfigMaps, source.ConfigMap.Name) + } + if source.Secret != nil { + refs.Secrets = append(refs.Secrets, source.Secret.Name) + } + } + } + } +} diff --git a/pkg/kor/argo_workflow_validation_test.go b/pkg/kor/argo_workflow_validation_test.go new file mode 100644 index 00000000..f1bad001 --- /dev/null +++ b/pkg/kor/argo_workflow_validation_test.go @@ -0,0 +1,295 @@ +package kor + +import ( + "testing" + + wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + dynamicfake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes/fake" +) + +func TestValidateArgoWorkflowTemplateAvailability(t *testing.T) { + scheme := runtime.NewScheme() + + // Test case: WorkflowTemplate CRD doesn't exist (simulated by fake client) + dynamicClient := dynamicfake.NewSimpleDynamicClient(scheme) + enabled := ValidateArgoWorkflowTemplateAvailability(dynamicClient) + + // Since we can't easily fake a CRD's existence in tests, we expect this to be false + if enabled { + t.Error("Expected ValidateArgoWorkflowTemplateAvailability to return false when CRD doesn't exist") + } +} + +func TestValidateResourceReferencesFromArgoWorkflowTemplates_NoCRD(t *testing.T) { + clientset := fake.NewSimpleClientset() + scheme := runtime.NewScheme() + dynamicClient := dynamicfake.NewSimpleDynamicClient(scheme) + + refs, err := ValidateResourceReferencesFromArgoWorkflowTemplates(clientset, dynamicClient, "test-namespace") + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + // When CRD doesn't exist, should return empty references + if len(refs.ConfigMaps) != 0 { + t.Errorf("Expected 0 ConfigMaps, got %d", len(refs.ConfigMaps)) + } + if len(refs.Secrets) != 0 { + t.Errorf("Expected 0 Secrets, got %d", len(refs.Secrets)) + } + if len(refs.PVCs) != 0 { + t.Errorf("Expected 0 PVCs, got %d", len(refs.PVCs)) + } +} + +func TestExtractResourceReferencesFromTypedWorkflowTemplate(t *testing.T) { + // Create a typed WorkflowTemplate with various resource references + wt := &wfv1.WorkflowTemplate{ + Spec: wfv1.WorkflowSpec{ + // Synchronization semaphore + Synchronization: &wfv1.Synchronization{ + Semaphore: &wfv1.SemaphoreRef{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "workflow-semaphore-config", + }, + Key: "workflow", + }, + }, + }, + // Global volumes + Volumes: []corev1.Volume{ + { + Name: "global-secret-volume", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "global-secret", + }, + }, + }, + { + Name: "global-config-volume", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "global-config", + }, + }, + }, + }, + }, + // Templates + Templates: []wfv1.Template{ + { + Name: "test-template", + Container: &corev1.Container{ + Name: "test-container", + Image: "busybox", + Env: []corev1.EnvVar{ + { + Name: "CONFIG_VALUE", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "app-config", + }, + Key: "config-key", + }, + }, + }, + { + Name: "SECRET_VALUE", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "app-secret", + }, + Key: "secret-key", + }, + }, + }, + }, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "env-config", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "env-secret", + }, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "secret-volume", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "volume-secret", + }, + }, + }, + { + Name: "config-volume", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "volume-config", + }, + }, + }, + }, + { + Name: "pvc-volume", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "test-pvc", + }, + }, + }, + { + Name: "projected-volume", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "projected-config", + }, + }, + }, + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "projected-secret", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + refs := &ArgoWorkflowTemplateResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + extractResourceReferencesFromTypedWorkflowTemplate(wt, refs) + + expectedConfigMaps := []string{ + "workflow-semaphore-config", + "global-config", + "app-config", + "env-config", + "volume-config", + "projected-config", + } + + expectedSecrets := []string{ + "global-secret", + "app-secret", + "env-secret", + "volume-secret", + "projected-secret", + } + + expectedPVCs := []string{ + "test-pvc", + } + + // Check ConfigMaps + if len(refs.ConfigMaps) != len(expectedConfigMaps) { + t.Errorf("Expected %d ConfigMaps, got %d: %v", len(expectedConfigMaps), len(refs.ConfigMaps), refs.ConfigMaps) + } + for _, expected := range expectedConfigMaps { + found := false + for _, actual := range refs.ConfigMaps { + if actual == expected { + found = true + break + } + } + if !found { + t.Errorf("Expected ConfigMap %q not found in refs: %v", expected, refs.ConfigMaps) + } + } + + // Check Secrets + if len(refs.Secrets) != len(expectedSecrets) { + t.Errorf("Expected %d Secrets, got %d: %v", len(expectedSecrets), len(refs.Secrets), refs.Secrets) + } + for _, expected := range expectedSecrets { + found := false + for _, actual := range refs.Secrets { + if actual == expected { + found = true + break + } + } + if !found { + t.Errorf("Expected Secret %q not found in refs: %v", expected, refs.Secrets) + } + } + + // Check PVCs + if len(refs.PVCs) != len(expectedPVCs) { + t.Errorf("Expected %d PVCs, got %d: %v", len(expectedPVCs), len(refs.PVCs), refs.PVCs) + } + for _, expected := range expectedPVCs { + found := false + for _, actual := range refs.PVCs { + if actual == expected { + found = true + break + } + } + if !found { + t.Errorf("Expected PVC %q not found in refs: %v", expected, refs.PVCs) + } + } +} + +func TestExtractResourceReferencesFromTypedWorkflowTemplate_Empty(t *testing.T) { + // Test with empty WorkflowTemplate + wt := &wfv1.WorkflowTemplate{ + Spec: wfv1.WorkflowSpec{ + Templates: []wfv1.Template{}, // Empty templates + }, + } + + refs := &ArgoWorkflowTemplateResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + extractResourceReferencesFromTypedWorkflowTemplate(wt, refs) + + // Should be no references in an empty spec + if len(refs.ConfigMaps) != 0 { + t.Errorf("Expected 0 ConfigMaps, got %d", len(refs.ConfigMaps)) + } + if len(refs.Secrets) != 0 { + t.Errorf("Expected 0 Secrets, got %d", len(refs.Secrets)) + } + if len(refs.PVCs) != 0 { + t.Errorf("Expected 0 PVCs, got %d", len(refs.PVCs)) + } +} diff --git a/pkg/kor/configmaps.go b/pkg/kor/configmaps.go index d85affbf..2f0e8343 100644 --- a/pkg/kor/configmaps.go +++ b/pkg/kor/configmaps.go @@ -9,6 +9,7 @@ import ( "os" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" @@ -19,7 +20,7 @@ import ( //go:embed exceptions/configmaps/configmaps.json var configMapsConfig []byte -func retrieveUsedCM(clientset kubernetes.Interface, namespace string) ([]string, []string, []string, []string, []string, error) { +func retrieveUsedCMFromPods(clientset kubernetes.Interface, namespace string) ([]string, []string, []string, []string, []string, error) { var volumesCM []string var envCM []string var envFromCM []string @@ -83,6 +84,14 @@ func retrieveUsedCM(clientset kubernetes.Interface, namespace string) ([]string, return volumesCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, nil } +func retrieveUsedCMFromArgoWorkflowTemplates(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string) ([]string, error) { + refs, err := ValidateResourceReferencesFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) + if err != nil { + return nil, err + } + return refs.ConfigMaps, nil +} + func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, []string, error) { configmaps, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels}) if err != nil { @@ -112,11 +121,19 @@ func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string, fi return names, unusedConfigmapNames, nil } -func processNamespaceCM(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ([]ResourceInfo, error) { - volumesCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, err := retrieveUsedCM(clientset, namespace) +func processNamespaceCM(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ([]ResourceInfo, error) { + // Retrieve ConfigMaps referenced by Pods + volumesCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, err := retrieveUsedCMFromPods(clientset, namespace) if err != nil { return nil, err } + + // Retrieve ConfigMaps referenced by Argo WorkflowTemplates + argoCM, err := retrieveUsedCMFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) + if err != nil { + return nil, err + } + config, err := unmarshalConfig(configMapsConfig) if err != nil { return nil, err @@ -127,6 +144,7 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter envFromCM = RemoveDuplicatesAndSort(envFromCM) envFromContainerCM = RemoveDuplicatesAndSort(envFromContainerCM) envFromInitContainerCM = RemoveDuplicatesAndSort(envFromInitContainerCM) + argoCM = RemoveDuplicatesAndSort(argoCM) configMapNames, unusedConfigmapNames, err := retrieveConfigMapNames(clientset, namespace, filterOpts) if err != nil { @@ -140,6 +158,7 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter envFromCM, envFromContainerCM, envFromInitContainerCM, + argoCM, } for _, slice := range slicesToAppend { @@ -175,10 +194,10 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter return diff, nil } -func GetUnusedConfigmaps(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts common.Opts) (string, error) { +func GetUnusedConfigmaps(filterOpts *filters.Options, clientset kubernetes.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { resources := make(map[string]map[string][]ResourceInfo) for _, namespace := range filterOpts.Namespaces(clientset) { - diff, err := processNamespaceCM(clientset, namespace, filterOpts, opts) + diff, err := processNamespaceCM(clientset, dynamicClient, namespace, filterOpts, opts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue diff --git a/pkg/kor/multi.go b/pkg/kor/multi.go index f3dbfbad..b9d6d44b 100644 --- a/pkg/kor/multi.go +++ b/pkg/kor/multi.go @@ -82,18 +82,18 @@ func retrieveNoNamespaceDiff(clientset kubernetes.Interface, apiExtClient apiext return noNamespaceDiff, clearedResourceList } -func retrieveNamespaceDiffs(clientset kubernetes.Interface, namespace string, resourceList []string, filterOpts *filters.Options, opts common.Opts) []ResourceDiff { +func retrieveNamespaceDiffs(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string, resourceList []string, filterOpts *filters.Options, opts common.Opts) []ResourceDiff { var allDiffs []ResourceDiff for _, resource := range resourceList { var diffResult ResourceDiff canonicalType := getCanonicalResourceType(resource) switch canonicalType { case "configmap": - diffResult = getUnusedCMs(clientset, namespace, filterOpts, opts) + diffResult = getUnusedCMs(clientset, dynamicClient, namespace, filterOpts, opts) case "service": diffResult = getUnusedSVCs(clientset, namespace, filterOpts, opts) case "secret": - diffResult = getUnusedSecrets(clientset, namespace, filterOpts, opts) + diffResult = getUnusedSecrets(clientset, dynamicClient, namespace, filterOpts, opts) case "serviceaccount": diffResult = getUnusedServiceAccounts(clientset, namespace, filterOpts, opts) case "deployment": @@ -105,7 +105,7 @@ func retrieveNamespaceDiffs(clientset kubernetes.Interface, namespace string, re case "horizontalpodautoscaler": diffResult = getUnusedHpas(clientset, namespace, filterOpts, opts) case "persistentvolumeclaim": - diffResult = getUnusedPvcs(clientset, namespace, filterOpts, opts) + diffResult = getUnusedPvcs(clientset, dynamicClient, namespace, filterOpts, opts) case "ingress": diffResult = getUnusedIngresses(clientset, namespace, filterOpts, opts) case "poddisruptionbudget": @@ -160,7 +160,7 @@ func GetUnusedMulti(resourceNames string, filterOpts *filters.Options, clientset } for _, namespace := range namespaces { - allDiffs := retrieveNamespaceDiffs(clientset, namespace, resourceList, filterOpts, opts) + allDiffs := retrieveNamespaceDiffs(clientset, dynamicClient, namespace, resourceList, filterOpts, opts) if opts.GroupBy == "namespace" { resources[namespace] = make(map[string][]ResourceInfo) } diff --git a/pkg/kor/pvc.go b/pkg/kor/pvc.go index 80861eb2..67d33682 100644 --- a/pkg/kor/pvc.go +++ b/pkg/kor/pvc.go @@ -8,6 +8,7 @@ import ( "os" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" @@ -15,7 +16,7 @@ import ( "github.com/yonahd/kor/pkg/filters" ) -func retrieveUsedPvcs(clientset kubernetes.Interface, namespace string) ([]string, error) { +func retrieveUsedPvcsFromPods(clientset kubernetes.Interface, namespace string) ([]string, error) { pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { fmt.Printf("Failed to list Pods: %v\n", err) @@ -38,7 +39,15 @@ func retrieveUsedPvcs(clientset kubernetes.Interface, namespace string) ([]strin return usedPvcs, err } -func processNamespacePvcs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ([]ResourceInfo, error) { +func retrieveUsedPvcsFromArgoWorkflowTemplates(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string) ([]string, error) { + refs, err := ValidateResourceReferencesFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) + if err != nil { + return nil, err + } + return refs.PVCs, nil +} + +func processNamespacePvcs(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ([]ResourceInfo, error) { pvcs, err := clientset.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels}) if err != nil { return nil, err @@ -64,13 +73,24 @@ func processNamespacePvcs(clientset kubernetes.Interface, namespace string, filt pvcNames = append(pvcNames, pvc.Name) } - usedPvcs, err := retrieveUsedPvcs(clientset, namespace) + // Retrieve PVCs referenced by Pods + podPvcs, err := retrieveUsedPvcsFromPods(clientset, namespace) if err != nil { return nil, err } + // Retrieve PVCs referenced by Argo WorkflowTemplates + argoPvcs, err := retrieveUsedPvcsFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) + if err != nil { + return nil, err + } + + // Combine all used PVCs + allUsedPvcs := append(podPvcs, argoPvcs...) + allUsedPvcs = RemoveDuplicatesAndSort(allUsedPvcs) + var diff []ResourceInfo - for _, name := range CalculateResourceDifference(usedPvcs, pvcNames) { + for _, name := range CalculateResourceDifference(allUsedPvcs, pvcNames) { reason := "PVC is not in use" diff = append(diff, ResourceInfo{Name: name, Reason: reason}) } @@ -88,10 +108,10 @@ func processNamespacePvcs(clientset kubernetes.Interface, namespace string, filt return diff, nil } -func GetUnusedPvcs(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts common.Opts) (string, error) { +func GetUnusedPvcs(filterOpts *filters.Options, clientset kubernetes.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { resources := make(map[string]map[string][]ResourceInfo) for _, namespace := range filterOpts.Namespaces(clientset) { - diff, err := processNamespacePvcs(clientset, namespace, filterOpts, opts) + diff, err := processNamespacePvcs(clientset, dynamicClient, namespace, filterOpts, opts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue diff --git a/pkg/kor/pvc_test.go b/pkg/kor/pvc_test.go index 47e2c319..18d364af 100644 --- a/pkg/kor/pvc_test.go +++ b/pkg/kor/pvc_test.go @@ -120,7 +120,7 @@ func TestGetUnusedPvcsStructured(t *testing.T) { GroupBy: "namespace", } - output, err := GetUnusedPvcs(&filters.Options{}, clientset, "json", opts) + output, err := GetUnusedPvcs(&filters.Options{}, clientset, nil, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedPvcsStructured: %v", err) } diff --git a/pkg/kor/secrets.go b/pkg/kor/secrets.go index 098e96f8..b2be1d60 100644 --- a/pkg/kor/secrets.go +++ b/pkg/kor/secrets.go @@ -9,6 +9,7 @@ import ( "os" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" "k8s.io/utils/strings/slices" @@ -45,7 +46,7 @@ func retrieveIngressTLS(clientset kubernetes.Interface, namespace string) ([]str } -func retrieveUsedSecret(clientset kubernetes.Interface, namespace string) ([]string, []string, []string, []string, []string, []string, error) { +func retrieveUsedSecretFromPods(clientset kubernetes.Interface, namespace string) ([]string, []string, []string, []string, []string, []string, error) { var envSecrets []string var envSecrets2 []string var volumeSecrets []string @@ -114,6 +115,14 @@ func retrieveUsedSecret(clientset kubernetes.Interface, namespace string) ([]str return envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, nil } +func retrieveUsedSecretFromArgoWorkflowTemplates(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string) ([]string, error) { + refs, err := ValidateResourceReferencesFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) + if err != nil { + return nil, err + } + return refs.Secrets, nil +} + func retrieveSecretNames(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, []string, error) { secrets, err := clientset.CoreV1().Secrets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels}) if err != nil { @@ -158,8 +167,15 @@ func retrieveSecretNames(clientset kubernetes.Interface, namespace string, filte return names, unusedSecretNames, nil } -func processNamespaceSecret(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ([]ResourceInfo, error) { - envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, err := retrieveUsedSecret(clientset, namespace) +func processNamespaceSecret(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ([]ResourceInfo, error) { + // Retrieve Secrets referenced by Pods + envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, err := retrieveUsedSecretFromPods(clientset, namespace) + if err != nil { + return nil, err + } + + // Retrieve Secrets referenced by Argo WorkflowTemplates + argoSecrets, err := retrieveUsedSecretFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) if err != nil { return nil, err } @@ -170,6 +186,7 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, fi initContainerEnvSecrets = RemoveDuplicatesAndSort(initContainerEnvSecrets) pullSecrets = RemoveDuplicatesAndSort(pullSecrets) tlsSecrets = RemoveDuplicatesAndSort(tlsSecrets) + argoSecrets = RemoveDuplicatesAndSort(argoSecrets) secretNames, unusedSecretNames, err := retrieveSecretNames(clientset, namespace, filterOpts) if err != nil { @@ -184,6 +201,7 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, fi pullSecrets, tlsSecrets, initContainerEnvSecrets, + argoSecrets, } for _, slice := range slicesToAppend { @@ -211,10 +229,10 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, fi } -func GetUnusedSecrets(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts common.Opts) (string, error) { +func GetUnusedSecrets(filterOpts *filters.Options, clientset kubernetes.Interface, dynamicClient dynamic.Interface, outputFormat string, opts common.Opts) (string, error) { resources := make(map[string]map[string][]ResourceInfo) for _, namespace := range filterOpts.Namespaces(clientset) { - diff, err := processNamespaceSecret(clientset, namespace, filterOpts, opts) + diff, err := processNamespaceSecret(clientset, dynamicClient, namespace, filterOpts, opts) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue diff --git a/pkg/kor/secrets_test.go b/pkg/kor/secrets_test.go index 15cf8895..b1925130 100644 --- a/pkg/kor/secrets_test.go +++ b/pkg/kor/secrets_test.go @@ -306,7 +306,7 @@ func TestGetUnusedSecretsStructured(t *testing.T) { GroupBy: "namespace", } - output, err := GetUnusedSecrets(&filters.Options{}, clientset, "json", opts) + output, err := GetUnusedSecrets(&filters.Options{}, clientset, nil, "json", opts) if err != nil { t.Fatalf("Error calling GetUnusedSecretsStructured: %v", err) }