From fdc38514e3322f0c6ed0df5dcf64b01df567e5b7 Mon Sep 17 00:00:00 2001 From: Jianan Wang Date: Fri, 15 Aug 2025 14:17:01 -0700 Subject: [PATCH 1/3] Initial commit for argo workflow support --- pkg/kor/configmaps.go | 22 ++ pkg/kor/externaldeps/argo_workflows.go | 261 ++++++++++++++++++ pkg/kor/externaldeps/argo_workflows_test.go | 278 ++++++++++++++++++++ pkg/kor/externaldeps/interface.go | 100 +++++++ pkg/kor/externaldeps/interface_test.go | 54 ++++ pkg/kor/externaldeps/registry.go | 32 +++ pkg/kor/externaldeps/registry_test.go | 254 ++++++++++++++++++ pkg/kor/pvc.go | 25 +- pkg/kor/secrets.go | 21 ++ 9 files changed, 1046 insertions(+), 1 deletion(-) create mode 100644 pkg/kor/externaldeps/argo_workflows.go create mode 100644 pkg/kor/externaldeps/argo_workflows_test.go create mode 100644 pkg/kor/externaldeps/interface.go create mode 100644 pkg/kor/externaldeps/interface_test.go create mode 100644 pkg/kor/externaldeps/registry.go create mode 100644 pkg/kor/externaldeps/registry_test.go diff --git a/pkg/kor/configmaps.go b/pkg/kor/configmaps.go index d85affbf..64a92b01 100644 --- a/pkg/kor/configmaps.go +++ b/pkg/kor/configmaps.go @@ -14,6 +14,7 @@ import ( "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" + "github.com/yonahd/kor/pkg/kor/externaldeps" ) //go:embed exceptions/configmaps/configmaps.json @@ -83,6 +84,18 @@ func retrieveUsedCM(clientset kubernetes.Interface, namespace string) ([]string, return volumesCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, nil } +func retrieveUsedCMFromExternalCRDs(clientset kubernetes.Interface, namespace string) ([]string, error) { + registry := externaldeps.GetGlobalRegistry() + dynamicClient := GetDynamicClient("") + + refs, err := registry.ScanNamespace(context.TODO(), namespace, clientset, dynamicClient) + if err != nil { + return nil, err + } + + return RemoveDuplicatesAndSort(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 { @@ -117,6 +130,13 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter if err != nil { return nil, err } + + // Retrieve ConfigMaps referenced by external CRDs (like Argo WorkflowTemplates) + externalCM, err := retrieveUsedCMFromExternalCRDs(clientset, namespace) + if err != nil { + return nil, err + } + config, err := unmarshalConfig(configMapsConfig) if err != nil { return nil, err @@ -127,6 +147,7 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter envFromCM = RemoveDuplicatesAndSort(envFromCM) envFromContainerCM = RemoveDuplicatesAndSort(envFromContainerCM) envFromInitContainerCM = RemoveDuplicatesAndSort(envFromInitContainerCM) + externalCM = RemoveDuplicatesAndSort(externalCM) configMapNames, unusedConfigmapNames, err := retrieveConfigMapNames(clientset, namespace, filterOpts) if err != nil { @@ -140,6 +161,7 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter envFromCM, envFromContainerCM, envFromInitContainerCM, + externalCM, } for _, slice := range slicesToAppend { diff --git a/pkg/kor/externaldeps/argo_workflows.go b/pkg/kor/externaldeps/argo_workflows.go new file mode 100644 index 00000000..78817a31 --- /dev/null +++ b/pkg/kor/externaldeps/argo_workflows.go @@ -0,0 +1,261 @@ +package externaldeps + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +const ( + argoWorkflowsAPIVersion = "argoproj.io/v1alpha1" + workflowTemplateKind = "WorkflowTemplate" +) + +// WorkflowTemplateScanner scans Argo Workflows WorkflowTemplate CRDs +// for references to ConfigMaps, Secrets, and PVCs +type WorkflowTemplateScanner struct { + gvr schema.GroupVersionResource +} + +// NewWorkflowTemplateScanner creates a new WorkflowTemplate scanner +func NewWorkflowTemplateScanner() *WorkflowTemplateScanner { + return &WorkflowTemplateScanner{ + gvr: schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "workflowtemplates", + }, + } +} + +// GetName returns the name of this scanner +func (s *WorkflowTemplateScanner) GetName() string { + return "Argo Workflows WorkflowTemplate" +} + +// IsEnabled checks if the WorkflowTemplate CRD is available in the cluster +func (s *WorkflowTemplateScanner) IsEnabled(ctx context.Context, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (bool, error) { + // Try to list WorkflowTemplates to check if the CRD exists + // We use a limit of 1 to avoid loading all resources, just to check availability + _, err := dynamicClient.Resource(s.gvr).Namespace("").List(ctx, metav1.ListOptions{Limit: 1}) + if err != nil { + // If we get an error, it likely means the CRD doesn't exist or we don't have permissions + return false, nil + } + return true, nil +} + +// ScanNamespace scans WorkflowTemplates in a specific namespace for resource references +func (s *WorkflowTemplateScanner) ScanNamespace(ctx context.Context, namespace string, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (*ResourceReferences, error) { + refs := &ResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + // List all WorkflowTemplates in the namespace + workflowTemplates, err := dynamicClient.Resource(s.gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list WorkflowTemplates in namespace %s: %v", namespace, err) + } + + // Scan each WorkflowTemplate for resource references + for _, wt := range workflowTemplates.Items { + s.extractResourceReferences(&wt, refs) + } + + return refs, nil +} + +// GetSupportedResources returns the resource types this scanner can find references to +func (s *WorkflowTemplateScanner) GetSupportedResources() []string { + return []string{"ConfigMap", "Secret", "PVC"} +} + +// extractResourceReferences extracts resource references from a WorkflowTemplate +func (s *WorkflowTemplateScanner) extractResourceReferences(wt *unstructured.Unstructured, refs *ResourceReferences) { + spec, found, err := unstructured.NestedMap(wt.Object, "spec") + if !found || err != nil { + return + } + + // Extract global level references + s.extractFromSpec(spec, refs) + + // Extract references from templates + templates, found, err := unstructured.NestedSlice(spec, "templates") + if found && err == nil { + for _, template := range templates { + if templateMap, ok := template.(map[string]interface{}); ok { + s.extractFromTemplate(templateMap, refs) + } + } + } + + // Extract references from volumes at the global level + volumes, found, err := unstructured.NestedSlice(spec, "volumes") + if found && err == nil { + s.extractFromVolumes(volumes, refs) + } +} + +// extractFromSpec extracts resource references from the WorkflowTemplate spec +func (s *WorkflowTemplateScanner) extractFromSpec(spec map[string]interface{}, refs *ResourceReferences) { + // Extract from synchronization.semaphore.configMapKeyRef + if syncMap, found := spec["synchronization"]; found { + if sync, ok := syncMap.(map[string]interface{}); ok { + if semaphoreMap, found := sync["semaphore"]; found { + if semaphore, ok := semaphoreMap.(map[string]interface{}); ok { + if configMapKeyRef, found := semaphore["configMapKeyRef"]; found { + if cmRef, ok := configMapKeyRef.(map[string]interface{}); ok { + if name, found := cmRef["name"]; found { + if nameStr, ok := name.(string); ok { + refs.ConfigMaps = append(refs.ConfigMaps, nameStr) + } + } + } + } + } + } + } + } +} + +// extractFromTemplate extracts resource references from a single template +func (s *WorkflowTemplateScanner) extractFromTemplate(template map[string]interface{}, refs *ResourceReferences) { + // Extract from script.env + if script, found := template["script"]; found { + if scriptMap, ok := script.(map[string]interface{}); ok { + s.extractFromEnv(scriptMap, refs) + } + } + + // Extract from container.env + if container, found := template["container"]; found { + if containerMap, ok := container.(map[string]interface{}); ok { + s.extractFromEnv(containerMap, refs) + } + } + + // Extract from volumes + if volumes, found := template["volumes"]; found { + if volumeSlice, ok := volumes.([]interface{}); ok { + s.extractFromVolumes(volumeSlice, refs) + } + } +} + +// extractFromEnv extracts resource references from environment variables +func (s *WorkflowTemplateScanner) extractFromEnv(container map[string]interface{}, refs *ResourceReferences) { + if env, found := container["env"]; found { + if envSlice, ok := env.([]interface{}); ok { + for _, envVar := range envSlice { + if envVarMap, ok := envVar.(map[string]interface{}); ok { + if valueFrom, found := envVarMap["valueFrom"]; found { + if valueFromMap, ok := valueFrom.(map[string]interface{}); ok { + // Check for configMapKeyRef + if configMapKeyRef, found := valueFromMap["configMapKeyRef"]; found { + if cmRef, ok := configMapKeyRef.(map[string]interface{}); ok { + if name, found := cmRef["name"]; found { + if nameStr, ok := name.(string); ok { + refs.ConfigMaps = append(refs.ConfigMaps, nameStr) + } + } + } + } + // Check for secretKeyRef + if secretKeyRef, found := valueFromMap["secretKeyRef"]; found { + if secretRef, ok := secretKeyRef.(map[string]interface{}); ok { + if name, found := secretRef["name"]; found { + if nameStr, ok := name.(string); ok { + refs.Secrets = append(refs.Secrets, nameStr) + } + } + } + } + } + } + } + } + } + } +} + +// extractFromVolumes extracts resource references from volumes +func (s *WorkflowTemplateScanner) extractFromVolumes(volumes []interface{}, refs *ResourceReferences) { + for _, volume := range volumes { + if volumeMap, ok := volume.(map[string]interface{}); ok { + // Check for secret volumes + if secret, found := volumeMap["secret"]; found { + if secretMap, ok := secret.(map[string]interface{}); ok { + if secretName, found := secretMap["secretName"]; found { + if nameStr, ok := secretName.(string); ok { + refs.Secrets = append(refs.Secrets, nameStr) + } + } + } + } + + // Check for configMap volumes + if configMap, found := volumeMap["configMap"]; found { + if cmMap, ok := configMap.(map[string]interface{}); ok { + if name, found := cmMap["name"]; found { + if nameStr, ok := name.(string); ok { + refs.ConfigMaps = append(refs.ConfigMaps, nameStr) + } + } + } + } + + // Check for PVC volumes + if pvc, found := volumeMap["persistentVolumeClaim"]; found { + if pvcMap, ok := pvc.(map[string]interface{}); ok { + if claimName, found := pvcMap["claimName"]; found { + if nameStr, ok := claimName.(string); ok { + refs.PVCs = append(refs.PVCs, nameStr) + } + } + } + } + + // Check for projected volumes (can contain configMaps and secrets) + if projected, found := volumeMap["projected"]; found { + if projectedMap, ok := projected.(map[string]interface{}); ok { + if sources, found := projectedMap["sources"]; found { + if sourcesSlice, ok := sources.([]interface{}); ok { + for _, source := range sourcesSlice { + if sourceMap, ok := source.(map[string]interface{}); ok { + // ConfigMap in projected volume + if configMap, found := sourceMap["configMap"]; found { + if cmMap, ok := configMap.(map[string]interface{}); ok { + if name, found := cmMap["name"]; found { + if nameStr, ok := name.(string); ok { + refs.ConfigMaps = append(refs.ConfigMaps, nameStr) + } + } + } + } + // Secret in projected volume + if secret, found := sourceMap["secret"]; found { + if secretMap, ok := secret.(map[string]interface{}); ok { + if name, found := secretMap["name"]; found { + if nameStr, ok := name.(string); ok { + refs.Secrets = append(refs.Secrets, nameStr) + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/pkg/kor/externaldeps/argo_workflows_test.go b/pkg/kor/externaldeps/argo_workflows_test.go new file mode 100644 index 00000000..3428fdd6 --- /dev/null +++ b/pkg/kor/externaldeps/argo_workflows_test.go @@ -0,0 +1,278 @@ +package externaldeps + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestWorkflowTemplateScanner_GetName(t *testing.T) { + scanner := NewWorkflowTemplateScanner() + expectedName := "Argo Workflows WorkflowTemplate" + if scanner.GetName() != expectedName { + t.Errorf("expected name %q, got %q", expectedName, scanner.GetName()) + } +} + +func TestWorkflowTemplateScanner_GetSupportedResources(t *testing.T) { + scanner := NewWorkflowTemplateScanner() + supportedResources := scanner.GetSupportedResources() + expected := []string{"ConfigMap", "Secret", "PVC"} + + if len(supportedResources) != len(expected) { + t.Errorf("expected %d supported resources, got %d", len(expected), len(supportedResources)) + return + } + + for i, resource := range expected { + if supportedResources[i] != resource { + t.Errorf("expected resource %q at index %d, got %q", resource, i, supportedResources[i]) + } + } +} + +func TestWorkflowTemplateScanner_extractResourceReferences(t *testing.T) { + scanner := NewWorkflowTemplateScanner() + + // Create a mock WorkflowTemplate with various resource references + wt := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "argoproj.io/v1alpha1", + "kind": "WorkflowTemplate", + "metadata": map[string]interface{}{ + "name": "test-workflow-template", + "namespace": "test-namespace", + }, + "spec": map[string]interface{}{ + "synchronization": map[string]interface{}{ + "semaphore": map[string]interface{}{ + "configMapKeyRef": map[string]interface{}{ + "name": "workflow-semaphore-config", + "key": "workflow", + }, + }, + }, + "templates": []interface{}{ + map[string]interface{}{ + "name": "test-template", + "script": map[string]interface{}{ + "env": []interface{}{ + map[string]interface{}{ + "name": "CONFIG_VALUE", + "valueFrom": map[string]interface{}{ + "configMapKeyRef": map[string]interface{}{ + "name": "app-config", + "key": "config-key", + }, + }, + }, + map[string]interface{}{ + "name": "SECRET_VALUE", + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "app-secret", + "key": "secret-key", + }, + }, + }, + }, + }, + "volumes": []interface{}{ + map[string]interface{}{ + "name": "secret-volume", + "secret": map[string]interface{}{ + "secretName": "volume-secret", + }, + }, + map[string]interface{}{ + "name": "config-volume", + "configMap": map[string]interface{}{ + "name": "volume-config", + }, + }, + map[string]interface{}{ + "name": "pvc-volume", + "persistentVolumeClaim": map[string]interface{}{ + "claimName": "test-pvc", + }, + }, + map[string]interface{}{ + "name": "projected-volume", + "projected": map[string]interface{}{ + "sources": []interface{}{ + map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": "projected-config", + }, + }, + map[string]interface{}{ + "secret": map[string]interface{}{ + "name": "projected-secret", + }, + }, + }, + }, + }, + }, + }, + }, + "volumes": []interface{}{ + map[string]interface{}{ + "name": "global-secret-volume", + "secret": map[string]interface{}{ + "secretName": "global-secret", + }, + }, + }, + }, + }, + } + + refs := &ResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + scanner.extractResourceReferences(wt, refs) + + expectedConfigMaps := []string{ + "workflow-semaphore-config", + "app-config", + "volume-config", + "projected-config", + } + + expectedSecrets := []string{ + "app-secret", + "volume-secret", + "projected-secret", + "global-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 TestWorkflowTemplateScanner_extractResourceReferences_EmptySpec(t *testing.T) { + scanner := NewWorkflowTemplateScanner() + + // Test with empty WorkflowTemplate + wt := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "argoproj.io/v1alpha1", + "kind": "WorkflowTemplate", + "metadata": map[string]interface{}{ + "name": "empty-workflow-template", + "namespace": "test-namespace", + }, + "spec": map[string]interface{}{}, + }, + } + + refs := &ResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + scanner.extractResourceReferences(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)) + } +} + +func TestWorkflowTemplateScanner_extractResourceReferences_MissingSpec(t *testing.T) { + scanner := NewWorkflowTemplateScanner() + + // Test with WorkflowTemplate missing spec + wt := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "argoproj.io/v1alpha1", + "kind": "WorkflowTemplate", + "metadata": map[string]interface{}{ + "name": "no-spec-workflow-template", + "namespace": "test-namespace", + }, + }, + } + + refs := &ResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + // Should not panic and should return no references + scanner.extractResourceReferences(wt, refs) + + 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/externaldeps/interface.go b/pkg/kor/externaldeps/interface.go new file mode 100644 index 00000000..9d0ed0eb --- /dev/null +++ b/pkg/kor/externaldeps/interface.go @@ -0,0 +1,100 @@ +package externaldeps + +import ( + "context" + + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +// ResourceReferences holds references to different types of Kubernetes resources +type ResourceReferences struct { + ConfigMaps []string + Secrets []string + PVCs []string + // We can extend this for other resource types in the future +} + +// ExternalResourceScanner defines the interface for scanning external CRDs +// that may reference standard Kubernetes resources +type ExternalResourceScanner interface { + // GetName returns a human-readable name for this scanner + GetName() string + + // IsEnabled checks if this scanner should be activated + // (e.g., by checking if the required CRD exists in the cluster) + IsEnabled(ctx context.Context, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (bool, error) + + // ScanNamespace scans a specific namespace for resource references + ScanNamespace(ctx context.Context, namespace string, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (*ResourceReferences, error) + + // GetSupportedResources returns the list of resource types this scanner can find references to + GetSupportedResources() []string +} + +// ScannerRegistry manages multiple external resource scanners +type ScannerRegistry struct { + scanners []ExternalResourceScanner +} + +// NewScannerRegistry creates a new scanner registry +func NewScannerRegistry() *ScannerRegistry { + return &ScannerRegistry{ + scanners: make([]ExternalResourceScanner, 0), + } +} + +// RegisterScanner registers a new external resource scanner +func (r *ScannerRegistry) RegisterScanner(scanner ExternalResourceScanner) { + r.scanners = append(r.scanners, scanner) +} + +// ScanNamespace scans a namespace using all registered and enabled scanners +func (r *ScannerRegistry) ScanNamespace(ctx context.Context, namespace string, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (*ResourceReferences, error) { + aggregatedRefs := &ResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + for _, scanner := range r.scanners { + enabled, err := scanner.IsEnabled(ctx, clientset, dynamicClient) + if err != nil { + return nil, err + } + + if !enabled { + continue + } + + refs, err := scanner.ScanNamespace(ctx, namespace, clientset, dynamicClient) + if err != nil { + return nil, err + } + + // Aggregate the references + aggregatedRefs.ConfigMaps = append(aggregatedRefs.ConfigMaps, refs.ConfigMaps...) + aggregatedRefs.Secrets = append(aggregatedRefs.Secrets, refs.Secrets...) + aggregatedRefs.PVCs = append(aggregatedRefs.PVCs, refs.PVCs...) + } + + return aggregatedRefs, nil +} + +// GetEnabledScanners returns a list of currently enabled scanners +func (r *ScannerRegistry) GetEnabledScanners(ctx context.Context, clientset kubernetes.Interface, dynamicClient dynamic.Interface) ([]ExternalResourceScanner, error) { + enabled := make([]ExternalResourceScanner, 0) + + for _, scanner := range r.scanners { + isEnabled, err := scanner.IsEnabled(ctx, clientset, dynamicClient) + if err != nil { + return nil, err + } + + if isEnabled { + enabled = append(enabled, scanner) + } + } + + return enabled, nil +} diff --git a/pkg/kor/externaldeps/interface_test.go b/pkg/kor/externaldeps/interface_test.go new file mode 100644 index 00000000..cf3f1918 --- /dev/null +++ b/pkg/kor/externaldeps/interface_test.go @@ -0,0 +1,54 @@ +package externaldeps + +import ( + "testing" +) + +func TestResourceReferences(t *testing.T) { + refs := &ResourceReferences{ + ConfigMaps: []string{"config1", "config2"}, + Secrets: []string{"secret1"}, + PVCs: []string{"pvc1", "pvc2"}, + } + + // Test that the struct properly holds the expected data + if len(refs.ConfigMaps) != 2 { + t.Errorf("expected 2 ConfigMaps, got %d", len(refs.ConfigMaps)) + } + if refs.ConfigMaps[0] != "config1" || refs.ConfigMaps[1] != "config2" { + t.Errorf("unexpected ConfigMaps: %v", refs.ConfigMaps) + } + + if len(refs.Secrets) != 1 { + t.Errorf("expected 1 Secret, got %d", len(refs.Secrets)) + } + if refs.Secrets[0] != "secret1" { + t.Errorf("unexpected Secret: %s", refs.Secrets[0]) + } + + if len(refs.PVCs) != 2 { + t.Errorf("expected 2 PVCs, got %d", len(refs.PVCs)) + } + if refs.PVCs[0] != "pvc1" || refs.PVCs[1] != "pvc2" { + t.Errorf("unexpected PVCs: %v", refs.PVCs) + } +} + +func TestResourceReferences_Empty(t *testing.T) { + refs := &ResourceReferences{ + ConfigMaps: make([]string, 0), + Secrets: make([]string, 0), + PVCs: make([]string, 0), + } + + // Test empty resource 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)) + } +} diff --git a/pkg/kor/externaldeps/registry.go b/pkg/kor/externaldeps/registry.go new file mode 100644 index 00000000..6e97d169 --- /dev/null +++ b/pkg/kor/externaldeps/registry.go @@ -0,0 +1,32 @@ +package externaldeps + +import ( + "sync" +) + +var ( + globalRegistry *ScannerRegistry + once sync.Once +) + +// GetGlobalRegistry returns the global scanner registry instance +// This follows the singleton pattern to ensure we have one registry across the application +func GetGlobalRegistry() *ScannerRegistry { + once.Do(func() { + globalRegistry = NewScannerRegistry() + // Register all available scanners + registerDefaultScanners() + }) + return globalRegistry +} + +// registerDefaultScanners registers all the default external resource scanners +func registerDefaultScanners() { + // Register Argo Workflows WorkflowTemplate scanner + globalRegistry.RegisterScanner(NewWorkflowTemplateScanner()) + + // Future scanners can be registered here: + // globalRegistry.RegisterScanner(NewArgoRolloutsScanner()) + // globalRegistry.RegisterScanner(NewTektonPipelineScanner()) + // etc. +} diff --git a/pkg/kor/externaldeps/registry_test.go b/pkg/kor/externaldeps/registry_test.go new file mode 100644 index 00000000..00e2a1f9 --- /dev/null +++ b/pkg/kor/externaldeps/registry_test.go @@ -0,0 +1,254 @@ +package externaldeps + +import ( + "context" + "testing" + + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +// mockScanner implements ExternalResourceScanner for testing +type mockScanner struct { + name string + enabled bool + enabledError error + scanResult *ResourceReferences + scanError error + supportedResources []string +} + +func (m *mockScanner) GetName() string { + return m.name +} + +func (m *mockScanner) IsEnabled(ctx context.Context, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (bool, error) { + return m.enabled, m.enabledError +} + +func (m *mockScanner) ScanNamespace(ctx context.Context, namespace string, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (*ResourceReferences, error) { + return m.scanResult, m.scanError +} + +func (m *mockScanner) GetSupportedResources() []string { + return m.supportedResources +} + +func TestNewScannerRegistry(t *testing.T) { + registry := NewScannerRegistry() + if registry == nil { + t.Error("expected non-nil registry") + } + if registry.scanners == nil { + t.Error("expected non-nil scanners slice") + } + if len(registry.scanners) != 0 { + t.Errorf("expected empty scanners slice, got %d scanners", len(registry.scanners)) + } +} + +func TestScannerRegistry_RegisterScanner(t *testing.T) { + registry := NewScannerRegistry() + + scanner1 := &mockScanner{name: "scanner1"} + scanner2 := &mockScanner{name: "scanner2"} + + registry.RegisterScanner(scanner1) + if len(registry.scanners) != 1 { + t.Errorf("expected 1 scanner, got %d", len(registry.scanners)) + } + + registry.RegisterScanner(scanner2) + if len(registry.scanners) != 2 { + t.Errorf("expected 2 scanners, got %d", len(registry.scanners)) + } + + // Verify the scanners are properly registered + if registry.scanners[0].GetName() != "scanner1" { + t.Errorf("expected first scanner name 'scanner1', got %q", registry.scanners[0].GetName()) + } + if registry.scanners[1].GetName() != "scanner2" { + t.Errorf("expected second scanner name 'scanner2', got %q", registry.scanners[1].GetName()) + } +} + +func TestScannerRegistry_ScanNamespace(t *testing.T) { + registry := NewScannerRegistry() + clientset := fake.NewSimpleClientset() + + // Create mock scanners + scanner1 := &mockScanner{ + name: "enabled-scanner", + enabled: true, + scanResult: &ResourceReferences{ + ConfigMaps: []string{"config1", "config2"}, + Secrets: []string{"secret1"}, + PVCs: []string{}, + }, + } + + scanner2 := &mockScanner{ + name: "disabled-scanner", + enabled: false, + scanResult: &ResourceReferences{ + ConfigMaps: []string{"should-not-appear"}, + Secrets: []string{"should-not-appear"}, + PVCs: []string{"should-not-appear"}, + }, + } + + scanner3 := &mockScanner{ + name: "another-enabled-scanner", + enabled: true, + scanResult: &ResourceReferences{ + ConfigMaps: []string{"config3"}, + Secrets: []string{"secret2", "secret3"}, + PVCs: []string{"pvc1"}, + }, + } + + registry.RegisterScanner(scanner1) + registry.RegisterScanner(scanner2) + registry.RegisterScanner(scanner3) + + refs, err := registry.ScanNamespace(context.TODO(), "test-namespace", clientset, nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + expectedConfigMaps := []string{"config1", "config2", "config3"} + expectedSecrets := []string{"secret1", "secret2", "secret3"} + expectedPVCs := []string{"pvc1"} + + // 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", expected) + } + } + + // 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", expected) + } + } + + // 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", expected) + } + } + + // Verify disabled scanner results are not included + for _, configMap := range refs.ConfigMaps { + if configMap == "should-not-appear" { + t.Error("disabled scanner results should not be included") + } + } +} + +func TestScannerRegistry_GetEnabledScanners(t *testing.T) { + registry := NewScannerRegistry() + clientset := fake.NewSimpleClientset() + + scanner1 := &mockScanner{name: "enabled1", enabled: true} + scanner2 := &mockScanner{name: "disabled1", enabled: false} + scanner3 := &mockScanner{name: "enabled2", enabled: true} + scanner4 := &mockScanner{name: "disabled2", enabled: false} + + registry.RegisterScanner(scanner1) + registry.RegisterScanner(scanner2) + registry.RegisterScanner(scanner3) + registry.RegisterScanner(scanner4) + + enabled, err := registry.GetEnabledScanners(context.TODO(), clientset, nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if len(enabled) != 2 { + t.Errorf("expected 2 enabled scanners, got %d", len(enabled)) + return + } + + // Check that only enabled scanners are returned + enabledNames := make(map[string]bool) + for _, scanner := range enabled { + enabledNames[scanner.GetName()] = true + } + + if !enabledNames["enabled1"] { + t.Error("expected 'enabled1' scanner to be in enabled list") + } + if !enabledNames["enabled2"] { + t.Error("expected 'enabled2' scanner to be in enabled list") + } + if enabledNames["disabled1"] { + t.Error("'disabled1' scanner should not be in enabled list") + } + if enabledNames["disabled2"] { + t.Error("'disabled2' scanner should not be in enabled list") + } +} + +func TestGetGlobalRegistry(t *testing.T) { + // Test that GetGlobalRegistry returns the same instance + registry1 := GetGlobalRegistry() + registry2 := GetGlobalRegistry() + + if registry1 != registry2 { + t.Error("GetGlobalRegistry should return the same instance (singleton pattern)") + } + + // Test that the registry is properly initialized with default scanners + if len(registry1.scanners) == 0 { + t.Error("expected global registry to have default scanners registered") + } + + // Check that WorkflowTemplate scanner is registered + found := false + for _, scanner := range registry1.scanners { + if scanner.GetName() == "Argo Workflows WorkflowTemplate" { + found = true + break + } + } + if !found { + t.Error("expected WorkflowTemplate scanner to be registered in global registry") + } +} diff --git a/pkg/kor/pvc.go b/pkg/kor/pvc.go index 80861eb2..eb77ec8b 100644 --- a/pkg/kor/pvc.go +++ b/pkg/kor/pvc.go @@ -13,6 +13,7 @@ import ( "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" + "github.com/yonahd/kor/pkg/kor/externaldeps" ) func retrieveUsedPvcs(clientset kubernetes.Interface, namespace string) ([]string, error) { @@ -38,6 +39,18 @@ func retrieveUsedPvcs(clientset kubernetes.Interface, namespace string) ([]strin return usedPvcs, err } +func retrieveUsedPvcsFromExternalCRDs(clientset kubernetes.Interface, namespace string) ([]string, error) { + registry := externaldeps.GetGlobalRegistry() + dynamicClient := GetDynamicClient("") + + refs, err := registry.ScanNamespace(context.TODO(), namespace, clientset, dynamicClient) + if err != nil { + return nil, err + } + + return RemoveDuplicatesAndSort(refs.PVCs), nil +} + func processNamespacePvcs(clientset kubernetes.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 { @@ -69,8 +82,18 @@ func processNamespacePvcs(clientset kubernetes.Interface, namespace string, filt return nil, err } + // Retrieve PVCs referenced by external CRDs (like Argo WorkflowTemplates) + externalPvcs, err := retrieveUsedPvcsFromExternalCRDs(clientset, namespace) + if err != nil { + return nil, err + } + + // Combine all used PVCs + allUsedPvcs := append(usedPvcs, externalPvcs...) + 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}) } diff --git a/pkg/kor/secrets.go b/pkg/kor/secrets.go index 098e96f8..aa1e7de1 100644 --- a/pkg/kor/secrets.go +++ b/pkg/kor/secrets.go @@ -15,6 +15,7 @@ import ( "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" + "github.com/yonahd/kor/pkg/kor/externaldeps" ) var exceptionSecretTypes = []string{ @@ -114,6 +115,18 @@ func retrieveUsedSecret(clientset kubernetes.Interface, namespace string) ([]str return envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, nil } +func retrieveUsedSecretsFromExternalCRDs(clientset kubernetes.Interface, namespace string) ([]string, error) { + registry := externaldeps.GetGlobalRegistry() + dynamicClient := GetDynamicClient("") + + refs, err := registry.ScanNamespace(context.TODO(), namespace, clientset, dynamicClient) + if err != nil { + return nil, err + } + + return RemoveDuplicatesAndSort(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 { @@ -164,12 +177,19 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, fi return nil, err } + // Retrieve Secrets referenced by external CRDs (like Argo WorkflowTemplates) + externalSecrets, err := retrieveUsedSecretsFromExternalCRDs(clientset, namespace) + if err != nil { + return nil, err + } + envSecrets = RemoveDuplicatesAndSort(envSecrets) envSecrets2 = RemoveDuplicatesAndSort(envSecrets2) volumeSecrets = RemoveDuplicatesAndSort(volumeSecrets) initContainerEnvSecrets = RemoveDuplicatesAndSort(initContainerEnvSecrets) pullSecrets = RemoveDuplicatesAndSort(pullSecrets) tlsSecrets = RemoveDuplicatesAndSort(tlsSecrets) + externalSecrets = RemoveDuplicatesAndSort(externalSecrets) secretNames, unusedSecretNames, err := retrieveSecretNames(clientset, namespace, filterOpts) if err != nil { @@ -184,6 +204,7 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, fi pullSecrets, tlsSecrets, initContainerEnvSecrets, + externalSecrets, } for _, slice := range slicesToAppend { From 2493b904b676d6a2168c32824182079ae02215c1 Mon Sep 17 00:00:00 2001 From: Jianan Wang Date: Fri, 15 Aug 2025 14:17:31 -0700 Subject: [PATCH 2/3] add README.md --- ARGO_WORKFLOWS_INTEGRATION.md | 322 ++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 ARGO_WORKFLOWS_INTEGRATION.md diff --git a/ARGO_WORKFLOWS_INTEGRATION.md b/ARGO_WORKFLOWS_INTEGRATION.md new file mode 100644 index 00000000..46628bb8 --- /dev/null +++ b/ARGO_WORKFLOWS_INTEGRATION.md @@ -0,0 +1,322 @@ +# Argo Workflows Integration + +This document describes the integration of Argo Workflows WorkflowTemplate CRD support into Kor, which prevents false positives when detecting unused ConfigMaps, Secrets, and PVCs that are referenced by WorkflowTemplates. + +## Overview + +Argo Workflows uses WorkflowTemplate CRDs to define reusable workflow specifications. These templates often reference Kubernetes resources like ConfigMaps, Secrets, and PersistentVolumeClaims (PVCs). Without understanding these references, Kor might incorrectly mark these resources as unused, leading to false positives. + +This integration adds intelligent scanning of WorkflowTemplate CRDs to identify resource dependencies and prevent false positives. + +## Features + +### ✅ Supported Resource References + +The integration detects references to the following resources in WorkflowTemplates: + +#### ConfigMaps +- Global synchronization semaphores: `spec.synchronization.semaphore.configMapKeyRef` +- Environment variables: `env[].valueFrom.configMapKeyRef` +- Volume mounts: `volumes[].configMap` +- Projected volumes: `volumes[].projected.sources[].configMap` + +#### Secrets +- Environment variables: `env[].valueFrom.secretKeyRef` +- Volume mounts: `volumes[].secret` +- Projected volumes: `volumes[].projected.sources[].secret` + +#### PersistentVolumeClaims +- Volume mounts: `volumes[].persistentVolumeClaim` + +### 🔄 Automatic Detection + +- The integration automatically detects if the WorkflowTemplate CRD is available in the cluster +- Only activates when Argo Workflows is installed and WorkflowTemplate CRD exists +- Gracefully handles clusters without Argo Workflows (no impact on performance) + +### 🔧 Modular Architecture + +- Built with a pluggable scanner architecture +- Easy to extend for other Argo Workflows CRDs (ClusterWorkflowTemplate, etc.) +- Can be extended for other workflow engines (Tekton, etc.) + +## How It Works + +### Architecture + +``` +┌─────────────────────────┐ +│ Kor Main Scanner │ +│ (ConfigMap/Secret/ │ +│ PVC Detection) │ +└─────────┬───────────────┘ + │ + v +┌─────────────────────────┐ +│ External Dependencies │ +│ Registry │ +└─────────┬───────────────┘ + │ + v +┌─────────────────────────┐ +│ WorkflowTemplate │ +│ Scanner │ +└─────────────────────────┘ +``` + +### Integration Points + +The integration hooks into the existing resource scanning logic at these points: + +1. **ConfigMap Scanning** (`pkg/kor/configmaps.go`) + - `retrieveUsedCMFromExternalCRDs()` function + - Integrated into `processNamespaceCM()` + +2. **Secret Scanning** (`pkg/kor/secrets.go`) + - `retrieveUsedSecretsFromExternalCRDs()` function + - Integrated into `processNamespaceSecret()` + +3. **PVC Scanning** (`pkg/kor/pvc.go`) + - `retrieveUsedPvcsFromExternalCRDs()` function + - Integrated into `processNamespacePvcs()` + +## Usage + +The integration works transparently - no changes to existing Kor commands are needed. + +### Examples + +#### Before Integration (False Positives) +```bash +$ kor configmap --include-namespaces production +Unused ConfigMaps in namespace "production": +- workflow-config # Actually used by WorkflowTemplate! +- app-settings # Actually used by WorkflowTemplate! +``` + +#### After Integration (Accurate Results) +```bash +$ kor configmap --include-namespaces production +No unused ConfigMaps found in namespace "production" +``` + +#### Test WorkflowTemplate Detection +```bash +$ kor all --include-namespaces argo-workflows-namespace +# Will automatically detect and scan WorkflowTemplates if CRD exists +``` + +## Supported WorkflowTemplate Patterns + +### Synchronization Semaphores +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +spec: + synchronization: + semaphore: + configMapKeyRef: + name: workflow-semaphore-config # ✅ Detected + key: workflow +``` + +### Environment Variables +```yaml +spec: + templates: + - name: my-template + script: + env: + - name: CONFIG_VALUE + valueFrom: + configMapKeyRef: + name: app-config # ✅ Detected + key: config-key + - name: SECRET_VALUE + valueFrom: + secretKeyRef: + name: app-secret # ✅ Detected + key: secret-key +``` + +### Volume Mounts +```yaml +spec: + templates: + - name: my-template + volumes: + - name: config-volume + configMap: + name: volume-config # ✅ Detected + - name: secret-volume + secret: + secretName: volume-secret # ✅ Detected + - name: data-volume + persistentVolumeClaim: + claimName: my-pvc # ✅ Detected +``` + +### Projected Volumes +```yaml +spec: + volumes: + - name: projected-volume + projected: + sources: + - configMap: + name: projected-config # ✅ Detected + - secret: + name: projected-secret # ✅ Detected +``` + +## Technical Implementation + +### Key Components + +#### 1. External Dependencies Framework (`pkg/kor/externaldeps/`) + +**Interface** (`interface.go`) +- `ExternalResourceScanner` interface for pluggable scanners +- `ResourceReferences` struct for holding resource references +- `ScannerRegistry` for managing multiple scanners + +**WorkflowTemplate Scanner** (`argo_workflows.go`) +- Implements `ExternalResourceScanner` interface +- Scans WorkflowTemplate CRDs for resource references +- Handles various referencing patterns + +**Registry** (`registry.go`) +- Global singleton registry for scanners +- Automatic registration of default scanners + +#### 2. Integration with Existing Scanners + +Each resource type's scanner has been extended with: +- `retrieveUsedFromExternalCRDs()` function +- Integration into the main processing function +- Proper deduplication and sorting of results + +### Error Handling + +- Graceful handling when WorkflowTemplate CRD doesn't exist +- Non-intrusive - if external scanning fails, traditional scanning continues +- Comprehensive error logging for debugging + +### Performance Considerations + +- CRD existence check is cached by the scanner registry +- Only active when WorkflowTemplate CRD is present +- Minimal overhead on clusters without Argo Workflows +- Efficient unstructured JSON parsing for CRD content + +## Testing + +Comprehensive test coverage includes: + +- **Unit Tests** (`*_test.go`) + - Scanner interface implementations + - Resource reference extraction logic + - Registry functionality + - Error handling scenarios + +- **Integration Tests** + - End-to-end resource scanning with mock WorkflowTemplates + - CRD detection and activation logic + - False positive prevention verification + +### Running Tests + +```bash +# Run all external dependencies tests +go test ./pkg/kor/externaldeps/... -v + +# Run full test suite +go test ./... -v +``` + +## Future Extensions + +The modular architecture makes it easy to add support for: + +### Additional Argo Workflows CRDs +- ClusterWorkflowTemplate +- CronWorkflow +- WorkflowEventBinding + +### Other Workflow Engines +- Tekton Pipelines +- GitHub Actions +- Jenkins X + +### Example: Adding ClusterWorkflowTemplate Support + +```go +// In pkg/kor/externaldeps/argo_workflows.go +type ClusterWorkflowTemplateScanner struct { + gvr schema.GroupVersionResource +} + +func NewClusterWorkflowTemplateScanner() *ClusterWorkflowTemplateScanner { + return &ClusterWorkflowTemplateScanner{ + gvr: schema.GroupVersionResource{ + Group: "argoproj.io", + Version: "v1alpha1", + Resource: "clusterworkflowtemplates", + }, + } +} + +// Implement ExternalResourceScanner interface... +``` + +```go +// In pkg/kor/externaldeps/registry.go +func registerDefaultScanners() { + globalRegistry.RegisterScanner(NewWorkflowTemplateScanner()) + globalRegistry.RegisterScanner(NewClusterWorkflowTemplateScanner()) // Add this +} +``` + +## Troubleshooting + +### Common Issues + +**1. WorkflowTemplate CRD not detected** +- Verify Argo Workflows is installed: `kubectl get crd workflowtemplates.argoproj.io` +- Check Kor has permissions to list WorkflowTemplates + +**2. Resources still marked as unused** +- Verify WorkflowTemplate references use correct field names +- Check if resources are in the same namespace as WorkflowTemplate +- Enable verbose logging to see scanning activity + +**3. Performance impact** +- Integration only runs when WorkflowTemplate CRD exists +- Check cluster resource usage if scanning is slow +- Consider namespace-specific scanning for large clusters + +### Debug Information + +Add debug logging to see external scanner activity: +```go +// In your code +registry := externaldeps.GetGlobalRegistry() +enabled, err := registry.GetEnabledScanners(ctx, clientset, dynamicClient) +fmt.Printf("Enabled external scanners: %v\n", enabled) +``` + +## Contributing + +To contribute to the Argo Workflows integration: + +1. Follow the existing code patterns in `pkg/kor/externaldeps/` +2. Add comprehensive unit tests for new scanners +3. Update this documentation for new features +4. Ensure backward compatibility with existing Kor functionality + +## Security Considerations + +- The integration uses the same RBAC permissions as the main Kor application +- No additional cluster permissions required beyond standard Kor requirements +- CRD content is parsed as unstructured data - no direct deserialization +- Follows Kubernetes security best practices for CRD access From 3104e0cbdc76273c5d71071e8ec9a488279cf188 Mon Sep 17 00:00:00 2001 From: Jianan Wang Date: Mon, 18 Aug 2025 00:23:53 -0700 Subject: [PATCH 3/3] Refactor the logics and fix nits --- ARGO_WORKFLOWS_INTEGRATION.md | 322 -------------------- README.md | 50 ++- cmd/kor/configmaps.go | 3 +- cmd/kor/pvc.go | 3 +- cmd/kor/secrets.go | 3 +- go.mod | 54 ++-- go.sum | 167 ++++++---- pkg/kor/all.go | 30 +- pkg/kor/argo_workflow_validation.go | 180 +++++++++++ pkg/kor/argo_workflow_validation_test.go | 295 ++++++++++++++++++ pkg/kor/configmaps.go | 31 +- pkg/kor/externaldeps/argo_workflows.go | 261 ---------------- pkg/kor/externaldeps/argo_workflows_test.go | 278 ----------------- pkg/kor/externaldeps/interface.go | 100 ------ pkg/kor/externaldeps/interface_test.go | 54 ---- pkg/kor/externaldeps/registry.go | 32 -- pkg/kor/externaldeps/registry_test.go | 254 --------------- pkg/kor/multi.go | 10 +- pkg/kor/pvc.go | 29 +- pkg/kor/pvc_test.go | 2 +- pkg/kor/secrets.go | 31 +- pkg/kor/secrets_test.go | 2 +- 22 files changed, 739 insertions(+), 1452 deletions(-) delete mode 100644 ARGO_WORKFLOWS_INTEGRATION.md create mode 100644 pkg/kor/argo_workflow_validation.go create mode 100644 pkg/kor/argo_workflow_validation_test.go delete mode 100644 pkg/kor/externaldeps/argo_workflows.go delete mode 100644 pkg/kor/externaldeps/argo_workflows_test.go delete mode 100644 pkg/kor/externaldeps/interface.go delete mode 100644 pkg/kor/externaldeps/interface_test.go delete mode 100644 pkg/kor/externaldeps/registry.go delete mode 100644 pkg/kor/externaldeps/registry_test.go diff --git a/ARGO_WORKFLOWS_INTEGRATION.md b/ARGO_WORKFLOWS_INTEGRATION.md deleted file mode 100644 index 46628bb8..00000000 --- a/ARGO_WORKFLOWS_INTEGRATION.md +++ /dev/null @@ -1,322 +0,0 @@ -# Argo Workflows Integration - -This document describes the integration of Argo Workflows WorkflowTemplate CRD support into Kor, which prevents false positives when detecting unused ConfigMaps, Secrets, and PVCs that are referenced by WorkflowTemplates. - -## Overview - -Argo Workflows uses WorkflowTemplate CRDs to define reusable workflow specifications. These templates often reference Kubernetes resources like ConfigMaps, Secrets, and PersistentVolumeClaims (PVCs). Without understanding these references, Kor might incorrectly mark these resources as unused, leading to false positives. - -This integration adds intelligent scanning of WorkflowTemplate CRDs to identify resource dependencies and prevent false positives. - -## Features - -### ✅ Supported Resource References - -The integration detects references to the following resources in WorkflowTemplates: - -#### ConfigMaps -- Global synchronization semaphores: `spec.synchronization.semaphore.configMapKeyRef` -- Environment variables: `env[].valueFrom.configMapKeyRef` -- Volume mounts: `volumes[].configMap` -- Projected volumes: `volumes[].projected.sources[].configMap` - -#### Secrets -- Environment variables: `env[].valueFrom.secretKeyRef` -- Volume mounts: `volumes[].secret` -- Projected volumes: `volumes[].projected.sources[].secret` - -#### PersistentVolumeClaims -- Volume mounts: `volumes[].persistentVolumeClaim` - -### 🔄 Automatic Detection - -- The integration automatically detects if the WorkflowTemplate CRD is available in the cluster -- Only activates when Argo Workflows is installed and WorkflowTemplate CRD exists -- Gracefully handles clusters without Argo Workflows (no impact on performance) - -### 🔧 Modular Architecture - -- Built with a pluggable scanner architecture -- Easy to extend for other Argo Workflows CRDs (ClusterWorkflowTemplate, etc.) -- Can be extended for other workflow engines (Tekton, etc.) - -## How It Works - -### Architecture - -``` -┌─────────────────────────┐ -│ Kor Main Scanner │ -│ (ConfigMap/Secret/ │ -│ PVC Detection) │ -└─────────┬───────────────┘ - │ - v -┌─────────────────────────┐ -│ External Dependencies │ -│ Registry │ -└─────────┬───────────────┘ - │ - v -┌─────────────────────────┐ -│ WorkflowTemplate │ -│ Scanner │ -└─────────────────────────┘ -``` - -### Integration Points - -The integration hooks into the existing resource scanning logic at these points: - -1. **ConfigMap Scanning** (`pkg/kor/configmaps.go`) - - `retrieveUsedCMFromExternalCRDs()` function - - Integrated into `processNamespaceCM()` - -2. **Secret Scanning** (`pkg/kor/secrets.go`) - - `retrieveUsedSecretsFromExternalCRDs()` function - - Integrated into `processNamespaceSecret()` - -3. **PVC Scanning** (`pkg/kor/pvc.go`) - - `retrieveUsedPvcsFromExternalCRDs()` function - - Integrated into `processNamespacePvcs()` - -## Usage - -The integration works transparently - no changes to existing Kor commands are needed. - -### Examples - -#### Before Integration (False Positives) -```bash -$ kor configmap --include-namespaces production -Unused ConfigMaps in namespace "production": -- workflow-config # Actually used by WorkflowTemplate! -- app-settings # Actually used by WorkflowTemplate! -``` - -#### After Integration (Accurate Results) -```bash -$ kor configmap --include-namespaces production -No unused ConfigMaps found in namespace "production" -``` - -#### Test WorkflowTemplate Detection -```bash -$ kor all --include-namespaces argo-workflows-namespace -# Will automatically detect and scan WorkflowTemplates if CRD exists -``` - -## Supported WorkflowTemplate Patterns - -### Synchronization Semaphores -```yaml -apiVersion: argoproj.io/v1alpha1 -kind: WorkflowTemplate -spec: - synchronization: - semaphore: - configMapKeyRef: - name: workflow-semaphore-config # ✅ Detected - key: workflow -``` - -### Environment Variables -```yaml -spec: - templates: - - name: my-template - script: - env: - - name: CONFIG_VALUE - valueFrom: - configMapKeyRef: - name: app-config # ✅ Detected - key: config-key - - name: SECRET_VALUE - valueFrom: - secretKeyRef: - name: app-secret # ✅ Detected - key: secret-key -``` - -### Volume Mounts -```yaml -spec: - templates: - - name: my-template - volumes: - - name: config-volume - configMap: - name: volume-config # ✅ Detected - - name: secret-volume - secret: - secretName: volume-secret # ✅ Detected - - name: data-volume - persistentVolumeClaim: - claimName: my-pvc # ✅ Detected -``` - -### Projected Volumes -```yaml -spec: - volumes: - - name: projected-volume - projected: - sources: - - configMap: - name: projected-config # ✅ Detected - - secret: - name: projected-secret # ✅ Detected -``` - -## Technical Implementation - -### Key Components - -#### 1. External Dependencies Framework (`pkg/kor/externaldeps/`) - -**Interface** (`interface.go`) -- `ExternalResourceScanner` interface for pluggable scanners -- `ResourceReferences` struct for holding resource references -- `ScannerRegistry` for managing multiple scanners - -**WorkflowTemplate Scanner** (`argo_workflows.go`) -- Implements `ExternalResourceScanner` interface -- Scans WorkflowTemplate CRDs for resource references -- Handles various referencing patterns - -**Registry** (`registry.go`) -- Global singleton registry for scanners -- Automatic registration of default scanners - -#### 2. Integration with Existing Scanners - -Each resource type's scanner has been extended with: -- `retrieveUsedFromExternalCRDs()` function -- Integration into the main processing function -- Proper deduplication and sorting of results - -### Error Handling - -- Graceful handling when WorkflowTemplate CRD doesn't exist -- Non-intrusive - if external scanning fails, traditional scanning continues -- Comprehensive error logging for debugging - -### Performance Considerations - -- CRD existence check is cached by the scanner registry -- Only active when WorkflowTemplate CRD is present -- Minimal overhead on clusters without Argo Workflows -- Efficient unstructured JSON parsing for CRD content - -## Testing - -Comprehensive test coverage includes: - -- **Unit Tests** (`*_test.go`) - - Scanner interface implementations - - Resource reference extraction logic - - Registry functionality - - Error handling scenarios - -- **Integration Tests** - - End-to-end resource scanning with mock WorkflowTemplates - - CRD detection and activation logic - - False positive prevention verification - -### Running Tests - -```bash -# Run all external dependencies tests -go test ./pkg/kor/externaldeps/... -v - -# Run full test suite -go test ./... -v -``` - -## Future Extensions - -The modular architecture makes it easy to add support for: - -### Additional Argo Workflows CRDs -- ClusterWorkflowTemplate -- CronWorkflow -- WorkflowEventBinding - -### Other Workflow Engines -- Tekton Pipelines -- GitHub Actions -- Jenkins X - -### Example: Adding ClusterWorkflowTemplate Support - -```go -// In pkg/kor/externaldeps/argo_workflows.go -type ClusterWorkflowTemplateScanner struct { - gvr schema.GroupVersionResource -} - -func NewClusterWorkflowTemplateScanner() *ClusterWorkflowTemplateScanner { - return &ClusterWorkflowTemplateScanner{ - gvr: schema.GroupVersionResource{ - Group: "argoproj.io", - Version: "v1alpha1", - Resource: "clusterworkflowtemplates", - }, - } -} - -// Implement ExternalResourceScanner interface... -``` - -```go -// In pkg/kor/externaldeps/registry.go -func registerDefaultScanners() { - globalRegistry.RegisterScanner(NewWorkflowTemplateScanner()) - globalRegistry.RegisterScanner(NewClusterWorkflowTemplateScanner()) // Add this -} -``` - -## Troubleshooting - -### Common Issues - -**1. WorkflowTemplate CRD not detected** -- Verify Argo Workflows is installed: `kubectl get crd workflowtemplates.argoproj.io` -- Check Kor has permissions to list WorkflowTemplates - -**2. Resources still marked as unused** -- Verify WorkflowTemplate references use correct field names -- Check if resources are in the same namespace as WorkflowTemplate -- Enable verbose logging to see scanning activity - -**3. Performance impact** -- Integration only runs when WorkflowTemplate CRD exists -- Check cluster resource usage if scanning is slow -- Consider namespace-specific scanning for large clusters - -### Debug Information - -Add debug logging to see external scanner activity: -```go -// In your code -registry := externaldeps.GetGlobalRegistry() -enabled, err := registry.GetEnabledScanners(ctx, clientset, dynamicClient) -fmt.Printf("Enabled external scanners: %v\n", enabled) -``` - -## Contributing - -To contribute to the Argo Workflows integration: - -1. Follow the existing code patterns in `pkg/kor/externaldeps/` -2. Add comprehensive unit tests for new scanners -3. Update this documentation for new features -4. Ensure backward compatibility with existing Kor functionality - -## Security Considerations - -- The integration uses the same RBAC permissions as the main Kor application -- No additional cluster permissions required beyond standard Kor requirements -- CRD content is parsed as unstructured data - no direct deserialization -- Follows Kubernetes security best practices for CRD access 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 64a92b01..2f0e8343 100644 --- a/pkg/kor/configmaps.go +++ b/pkg/kor/configmaps.go @@ -9,18 +9,18 @@ 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" "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" - "github.com/yonahd/kor/pkg/kor/externaldeps" ) //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 @@ -84,16 +84,12 @@ func retrieveUsedCM(clientset kubernetes.Interface, namespace string) ([]string, return volumesCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, nil } -func retrieveUsedCMFromExternalCRDs(clientset kubernetes.Interface, namespace string) ([]string, error) { - registry := externaldeps.GetGlobalRegistry() - dynamicClient := GetDynamicClient("") - - refs, err := registry.ScanNamespace(context.TODO(), namespace, clientset, dynamicClient) +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 RemoveDuplicatesAndSort(refs.ConfigMaps), nil + return refs.ConfigMaps, nil } func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, []string, error) { @@ -125,14 +121,15 @@ 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 external CRDs (like Argo WorkflowTemplates) - externalCM, err := retrieveUsedCMFromExternalCRDs(clientset, namespace) + // Retrieve ConfigMaps referenced by Argo WorkflowTemplates + argoCM, err := retrieveUsedCMFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) if err != nil { return nil, err } @@ -147,7 +144,7 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter envFromCM = RemoveDuplicatesAndSort(envFromCM) envFromContainerCM = RemoveDuplicatesAndSort(envFromContainerCM) envFromInitContainerCM = RemoveDuplicatesAndSort(envFromInitContainerCM) - externalCM = RemoveDuplicatesAndSort(externalCM) + argoCM = RemoveDuplicatesAndSort(argoCM) configMapNames, unusedConfigmapNames, err := retrieveConfigMapNames(clientset, namespace, filterOpts) if err != nil { @@ -161,7 +158,7 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter envFromCM, envFromContainerCM, envFromInitContainerCM, - externalCM, + argoCM, } for _, slice := range slicesToAppend { @@ -197,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/externaldeps/argo_workflows.go b/pkg/kor/externaldeps/argo_workflows.go deleted file mode 100644 index 78817a31..00000000 --- a/pkg/kor/externaldeps/argo_workflows.go +++ /dev/null @@ -1,261 +0,0 @@ -package externaldeps - -import ( - "context" - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" -) - -const ( - argoWorkflowsAPIVersion = "argoproj.io/v1alpha1" - workflowTemplateKind = "WorkflowTemplate" -) - -// WorkflowTemplateScanner scans Argo Workflows WorkflowTemplate CRDs -// for references to ConfigMaps, Secrets, and PVCs -type WorkflowTemplateScanner struct { - gvr schema.GroupVersionResource -} - -// NewWorkflowTemplateScanner creates a new WorkflowTemplate scanner -func NewWorkflowTemplateScanner() *WorkflowTemplateScanner { - return &WorkflowTemplateScanner{ - gvr: schema.GroupVersionResource{ - Group: "argoproj.io", - Version: "v1alpha1", - Resource: "workflowtemplates", - }, - } -} - -// GetName returns the name of this scanner -func (s *WorkflowTemplateScanner) GetName() string { - return "Argo Workflows WorkflowTemplate" -} - -// IsEnabled checks if the WorkflowTemplate CRD is available in the cluster -func (s *WorkflowTemplateScanner) IsEnabled(ctx context.Context, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (bool, error) { - // Try to list WorkflowTemplates to check if the CRD exists - // We use a limit of 1 to avoid loading all resources, just to check availability - _, err := dynamicClient.Resource(s.gvr).Namespace("").List(ctx, metav1.ListOptions{Limit: 1}) - if err != nil { - // If we get an error, it likely means the CRD doesn't exist or we don't have permissions - return false, nil - } - return true, nil -} - -// ScanNamespace scans WorkflowTemplates in a specific namespace for resource references -func (s *WorkflowTemplateScanner) ScanNamespace(ctx context.Context, namespace string, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (*ResourceReferences, error) { - refs := &ResourceReferences{ - ConfigMaps: make([]string, 0), - Secrets: make([]string, 0), - PVCs: make([]string, 0), - } - - // List all WorkflowTemplates in the namespace - workflowTemplates, err := dynamicClient.Resource(s.gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, fmt.Errorf("failed to list WorkflowTemplates in namespace %s: %v", namespace, err) - } - - // Scan each WorkflowTemplate for resource references - for _, wt := range workflowTemplates.Items { - s.extractResourceReferences(&wt, refs) - } - - return refs, nil -} - -// GetSupportedResources returns the resource types this scanner can find references to -func (s *WorkflowTemplateScanner) GetSupportedResources() []string { - return []string{"ConfigMap", "Secret", "PVC"} -} - -// extractResourceReferences extracts resource references from a WorkflowTemplate -func (s *WorkflowTemplateScanner) extractResourceReferences(wt *unstructured.Unstructured, refs *ResourceReferences) { - spec, found, err := unstructured.NestedMap(wt.Object, "spec") - if !found || err != nil { - return - } - - // Extract global level references - s.extractFromSpec(spec, refs) - - // Extract references from templates - templates, found, err := unstructured.NestedSlice(spec, "templates") - if found && err == nil { - for _, template := range templates { - if templateMap, ok := template.(map[string]interface{}); ok { - s.extractFromTemplate(templateMap, refs) - } - } - } - - // Extract references from volumes at the global level - volumes, found, err := unstructured.NestedSlice(spec, "volumes") - if found && err == nil { - s.extractFromVolumes(volumes, refs) - } -} - -// extractFromSpec extracts resource references from the WorkflowTemplate spec -func (s *WorkflowTemplateScanner) extractFromSpec(spec map[string]interface{}, refs *ResourceReferences) { - // Extract from synchronization.semaphore.configMapKeyRef - if syncMap, found := spec["synchronization"]; found { - if sync, ok := syncMap.(map[string]interface{}); ok { - if semaphoreMap, found := sync["semaphore"]; found { - if semaphore, ok := semaphoreMap.(map[string]interface{}); ok { - if configMapKeyRef, found := semaphore["configMapKeyRef"]; found { - if cmRef, ok := configMapKeyRef.(map[string]interface{}); ok { - if name, found := cmRef["name"]; found { - if nameStr, ok := name.(string); ok { - refs.ConfigMaps = append(refs.ConfigMaps, nameStr) - } - } - } - } - } - } - } - } -} - -// extractFromTemplate extracts resource references from a single template -func (s *WorkflowTemplateScanner) extractFromTemplate(template map[string]interface{}, refs *ResourceReferences) { - // Extract from script.env - if script, found := template["script"]; found { - if scriptMap, ok := script.(map[string]interface{}); ok { - s.extractFromEnv(scriptMap, refs) - } - } - - // Extract from container.env - if container, found := template["container"]; found { - if containerMap, ok := container.(map[string]interface{}); ok { - s.extractFromEnv(containerMap, refs) - } - } - - // Extract from volumes - if volumes, found := template["volumes"]; found { - if volumeSlice, ok := volumes.([]interface{}); ok { - s.extractFromVolumes(volumeSlice, refs) - } - } -} - -// extractFromEnv extracts resource references from environment variables -func (s *WorkflowTemplateScanner) extractFromEnv(container map[string]interface{}, refs *ResourceReferences) { - if env, found := container["env"]; found { - if envSlice, ok := env.([]interface{}); ok { - for _, envVar := range envSlice { - if envVarMap, ok := envVar.(map[string]interface{}); ok { - if valueFrom, found := envVarMap["valueFrom"]; found { - if valueFromMap, ok := valueFrom.(map[string]interface{}); ok { - // Check for configMapKeyRef - if configMapKeyRef, found := valueFromMap["configMapKeyRef"]; found { - if cmRef, ok := configMapKeyRef.(map[string]interface{}); ok { - if name, found := cmRef["name"]; found { - if nameStr, ok := name.(string); ok { - refs.ConfigMaps = append(refs.ConfigMaps, nameStr) - } - } - } - } - // Check for secretKeyRef - if secretKeyRef, found := valueFromMap["secretKeyRef"]; found { - if secretRef, ok := secretKeyRef.(map[string]interface{}); ok { - if name, found := secretRef["name"]; found { - if nameStr, ok := name.(string); ok { - refs.Secrets = append(refs.Secrets, nameStr) - } - } - } - } - } - } - } - } - } - } -} - -// extractFromVolumes extracts resource references from volumes -func (s *WorkflowTemplateScanner) extractFromVolumes(volumes []interface{}, refs *ResourceReferences) { - for _, volume := range volumes { - if volumeMap, ok := volume.(map[string]interface{}); ok { - // Check for secret volumes - if secret, found := volumeMap["secret"]; found { - if secretMap, ok := secret.(map[string]interface{}); ok { - if secretName, found := secretMap["secretName"]; found { - if nameStr, ok := secretName.(string); ok { - refs.Secrets = append(refs.Secrets, nameStr) - } - } - } - } - - // Check for configMap volumes - if configMap, found := volumeMap["configMap"]; found { - if cmMap, ok := configMap.(map[string]interface{}); ok { - if name, found := cmMap["name"]; found { - if nameStr, ok := name.(string); ok { - refs.ConfigMaps = append(refs.ConfigMaps, nameStr) - } - } - } - } - - // Check for PVC volumes - if pvc, found := volumeMap["persistentVolumeClaim"]; found { - if pvcMap, ok := pvc.(map[string]interface{}); ok { - if claimName, found := pvcMap["claimName"]; found { - if nameStr, ok := claimName.(string); ok { - refs.PVCs = append(refs.PVCs, nameStr) - } - } - } - } - - // Check for projected volumes (can contain configMaps and secrets) - if projected, found := volumeMap["projected"]; found { - if projectedMap, ok := projected.(map[string]interface{}); ok { - if sources, found := projectedMap["sources"]; found { - if sourcesSlice, ok := sources.([]interface{}); ok { - for _, source := range sourcesSlice { - if sourceMap, ok := source.(map[string]interface{}); ok { - // ConfigMap in projected volume - if configMap, found := sourceMap["configMap"]; found { - if cmMap, ok := configMap.(map[string]interface{}); ok { - if name, found := cmMap["name"]; found { - if nameStr, ok := name.(string); ok { - refs.ConfigMaps = append(refs.ConfigMaps, nameStr) - } - } - } - } - // Secret in projected volume - if secret, found := sourceMap["secret"]; found { - if secretMap, ok := secret.(map[string]interface{}); ok { - if name, found := secretMap["name"]; found { - if nameStr, ok := name.(string); ok { - refs.Secrets = append(refs.Secrets, nameStr) - } - } - } - } - } - } - } - } - } - } - } - } -} diff --git a/pkg/kor/externaldeps/argo_workflows_test.go b/pkg/kor/externaldeps/argo_workflows_test.go deleted file mode 100644 index 3428fdd6..00000000 --- a/pkg/kor/externaldeps/argo_workflows_test.go +++ /dev/null @@ -1,278 +0,0 @@ -package externaldeps - -import ( - "testing" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestWorkflowTemplateScanner_GetName(t *testing.T) { - scanner := NewWorkflowTemplateScanner() - expectedName := "Argo Workflows WorkflowTemplate" - if scanner.GetName() != expectedName { - t.Errorf("expected name %q, got %q", expectedName, scanner.GetName()) - } -} - -func TestWorkflowTemplateScanner_GetSupportedResources(t *testing.T) { - scanner := NewWorkflowTemplateScanner() - supportedResources := scanner.GetSupportedResources() - expected := []string{"ConfigMap", "Secret", "PVC"} - - if len(supportedResources) != len(expected) { - t.Errorf("expected %d supported resources, got %d", len(expected), len(supportedResources)) - return - } - - for i, resource := range expected { - if supportedResources[i] != resource { - t.Errorf("expected resource %q at index %d, got %q", resource, i, supportedResources[i]) - } - } -} - -func TestWorkflowTemplateScanner_extractResourceReferences(t *testing.T) { - scanner := NewWorkflowTemplateScanner() - - // Create a mock WorkflowTemplate with various resource references - wt := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "argoproj.io/v1alpha1", - "kind": "WorkflowTemplate", - "metadata": map[string]interface{}{ - "name": "test-workflow-template", - "namespace": "test-namespace", - }, - "spec": map[string]interface{}{ - "synchronization": map[string]interface{}{ - "semaphore": map[string]interface{}{ - "configMapKeyRef": map[string]interface{}{ - "name": "workflow-semaphore-config", - "key": "workflow", - }, - }, - }, - "templates": []interface{}{ - map[string]interface{}{ - "name": "test-template", - "script": map[string]interface{}{ - "env": []interface{}{ - map[string]interface{}{ - "name": "CONFIG_VALUE", - "valueFrom": map[string]interface{}{ - "configMapKeyRef": map[string]interface{}{ - "name": "app-config", - "key": "config-key", - }, - }, - }, - map[string]interface{}{ - "name": "SECRET_VALUE", - "valueFrom": map[string]interface{}{ - "secretKeyRef": map[string]interface{}{ - "name": "app-secret", - "key": "secret-key", - }, - }, - }, - }, - }, - "volumes": []interface{}{ - map[string]interface{}{ - "name": "secret-volume", - "secret": map[string]interface{}{ - "secretName": "volume-secret", - }, - }, - map[string]interface{}{ - "name": "config-volume", - "configMap": map[string]interface{}{ - "name": "volume-config", - }, - }, - map[string]interface{}{ - "name": "pvc-volume", - "persistentVolumeClaim": map[string]interface{}{ - "claimName": "test-pvc", - }, - }, - map[string]interface{}{ - "name": "projected-volume", - "projected": map[string]interface{}{ - "sources": []interface{}{ - map[string]interface{}{ - "configMap": map[string]interface{}{ - "name": "projected-config", - }, - }, - map[string]interface{}{ - "secret": map[string]interface{}{ - "name": "projected-secret", - }, - }, - }, - }, - }, - }, - }, - }, - "volumes": []interface{}{ - map[string]interface{}{ - "name": "global-secret-volume", - "secret": map[string]interface{}{ - "secretName": "global-secret", - }, - }, - }, - }, - }, - } - - refs := &ResourceReferences{ - ConfigMaps: make([]string, 0), - Secrets: make([]string, 0), - PVCs: make([]string, 0), - } - - scanner.extractResourceReferences(wt, refs) - - expectedConfigMaps := []string{ - "workflow-semaphore-config", - "app-config", - "volume-config", - "projected-config", - } - - expectedSecrets := []string{ - "app-secret", - "volume-secret", - "projected-secret", - "global-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 TestWorkflowTemplateScanner_extractResourceReferences_EmptySpec(t *testing.T) { - scanner := NewWorkflowTemplateScanner() - - // Test with empty WorkflowTemplate - wt := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "argoproj.io/v1alpha1", - "kind": "WorkflowTemplate", - "metadata": map[string]interface{}{ - "name": "empty-workflow-template", - "namespace": "test-namespace", - }, - "spec": map[string]interface{}{}, - }, - } - - refs := &ResourceReferences{ - ConfigMaps: make([]string, 0), - Secrets: make([]string, 0), - PVCs: make([]string, 0), - } - - scanner.extractResourceReferences(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)) - } -} - -func TestWorkflowTemplateScanner_extractResourceReferences_MissingSpec(t *testing.T) { - scanner := NewWorkflowTemplateScanner() - - // Test with WorkflowTemplate missing spec - wt := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "argoproj.io/v1alpha1", - "kind": "WorkflowTemplate", - "metadata": map[string]interface{}{ - "name": "no-spec-workflow-template", - "namespace": "test-namespace", - }, - }, - } - - refs := &ResourceReferences{ - ConfigMaps: make([]string, 0), - Secrets: make([]string, 0), - PVCs: make([]string, 0), - } - - // Should not panic and should return no references - scanner.extractResourceReferences(wt, refs) - - 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/externaldeps/interface.go b/pkg/kor/externaldeps/interface.go deleted file mode 100644 index 9d0ed0eb..00000000 --- a/pkg/kor/externaldeps/interface.go +++ /dev/null @@ -1,100 +0,0 @@ -package externaldeps - -import ( - "context" - - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" -) - -// ResourceReferences holds references to different types of Kubernetes resources -type ResourceReferences struct { - ConfigMaps []string - Secrets []string - PVCs []string - // We can extend this for other resource types in the future -} - -// ExternalResourceScanner defines the interface for scanning external CRDs -// that may reference standard Kubernetes resources -type ExternalResourceScanner interface { - // GetName returns a human-readable name for this scanner - GetName() string - - // IsEnabled checks if this scanner should be activated - // (e.g., by checking if the required CRD exists in the cluster) - IsEnabled(ctx context.Context, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (bool, error) - - // ScanNamespace scans a specific namespace for resource references - ScanNamespace(ctx context.Context, namespace string, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (*ResourceReferences, error) - - // GetSupportedResources returns the list of resource types this scanner can find references to - GetSupportedResources() []string -} - -// ScannerRegistry manages multiple external resource scanners -type ScannerRegistry struct { - scanners []ExternalResourceScanner -} - -// NewScannerRegistry creates a new scanner registry -func NewScannerRegistry() *ScannerRegistry { - return &ScannerRegistry{ - scanners: make([]ExternalResourceScanner, 0), - } -} - -// RegisterScanner registers a new external resource scanner -func (r *ScannerRegistry) RegisterScanner(scanner ExternalResourceScanner) { - r.scanners = append(r.scanners, scanner) -} - -// ScanNamespace scans a namespace using all registered and enabled scanners -func (r *ScannerRegistry) ScanNamespace(ctx context.Context, namespace string, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (*ResourceReferences, error) { - aggregatedRefs := &ResourceReferences{ - ConfigMaps: make([]string, 0), - Secrets: make([]string, 0), - PVCs: make([]string, 0), - } - - for _, scanner := range r.scanners { - enabled, err := scanner.IsEnabled(ctx, clientset, dynamicClient) - if err != nil { - return nil, err - } - - if !enabled { - continue - } - - refs, err := scanner.ScanNamespace(ctx, namespace, clientset, dynamicClient) - if err != nil { - return nil, err - } - - // Aggregate the references - aggregatedRefs.ConfigMaps = append(aggregatedRefs.ConfigMaps, refs.ConfigMaps...) - aggregatedRefs.Secrets = append(aggregatedRefs.Secrets, refs.Secrets...) - aggregatedRefs.PVCs = append(aggregatedRefs.PVCs, refs.PVCs...) - } - - return aggregatedRefs, nil -} - -// GetEnabledScanners returns a list of currently enabled scanners -func (r *ScannerRegistry) GetEnabledScanners(ctx context.Context, clientset kubernetes.Interface, dynamicClient dynamic.Interface) ([]ExternalResourceScanner, error) { - enabled := make([]ExternalResourceScanner, 0) - - for _, scanner := range r.scanners { - isEnabled, err := scanner.IsEnabled(ctx, clientset, dynamicClient) - if err != nil { - return nil, err - } - - if isEnabled { - enabled = append(enabled, scanner) - } - } - - return enabled, nil -} diff --git a/pkg/kor/externaldeps/interface_test.go b/pkg/kor/externaldeps/interface_test.go deleted file mode 100644 index cf3f1918..00000000 --- a/pkg/kor/externaldeps/interface_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package externaldeps - -import ( - "testing" -) - -func TestResourceReferences(t *testing.T) { - refs := &ResourceReferences{ - ConfigMaps: []string{"config1", "config2"}, - Secrets: []string{"secret1"}, - PVCs: []string{"pvc1", "pvc2"}, - } - - // Test that the struct properly holds the expected data - if len(refs.ConfigMaps) != 2 { - t.Errorf("expected 2 ConfigMaps, got %d", len(refs.ConfigMaps)) - } - if refs.ConfigMaps[0] != "config1" || refs.ConfigMaps[1] != "config2" { - t.Errorf("unexpected ConfigMaps: %v", refs.ConfigMaps) - } - - if len(refs.Secrets) != 1 { - t.Errorf("expected 1 Secret, got %d", len(refs.Secrets)) - } - if refs.Secrets[0] != "secret1" { - t.Errorf("unexpected Secret: %s", refs.Secrets[0]) - } - - if len(refs.PVCs) != 2 { - t.Errorf("expected 2 PVCs, got %d", len(refs.PVCs)) - } - if refs.PVCs[0] != "pvc1" || refs.PVCs[1] != "pvc2" { - t.Errorf("unexpected PVCs: %v", refs.PVCs) - } -} - -func TestResourceReferences_Empty(t *testing.T) { - refs := &ResourceReferences{ - ConfigMaps: make([]string, 0), - Secrets: make([]string, 0), - PVCs: make([]string, 0), - } - - // Test empty resource 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)) - } -} diff --git a/pkg/kor/externaldeps/registry.go b/pkg/kor/externaldeps/registry.go deleted file mode 100644 index 6e97d169..00000000 --- a/pkg/kor/externaldeps/registry.go +++ /dev/null @@ -1,32 +0,0 @@ -package externaldeps - -import ( - "sync" -) - -var ( - globalRegistry *ScannerRegistry - once sync.Once -) - -// GetGlobalRegistry returns the global scanner registry instance -// This follows the singleton pattern to ensure we have one registry across the application -func GetGlobalRegistry() *ScannerRegistry { - once.Do(func() { - globalRegistry = NewScannerRegistry() - // Register all available scanners - registerDefaultScanners() - }) - return globalRegistry -} - -// registerDefaultScanners registers all the default external resource scanners -func registerDefaultScanners() { - // Register Argo Workflows WorkflowTemplate scanner - globalRegistry.RegisterScanner(NewWorkflowTemplateScanner()) - - // Future scanners can be registered here: - // globalRegistry.RegisterScanner(NewArgoRolloutsScanner()) - // globalRegistry.RegisterScanner(NewTektonPipelineScanner()) - // etc. -} diff --git a/pkg/kor/externaldeps/registry_test.go b/pkg/kor/externaldeps/registry_test.go deleted file mode 100644 index 00e2a1f9..00000000 --- a/pkg/kor/externaldeps/registry_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package externaldeps - -import ( - "context" - "testing" - - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/fake" -) - -// mockScanner implements ExternalResourceScanner for testing -type mockScanner struct { - name string - enabled bool - enabledError error - scanResult *ResourceReferences - scanError error - supportedResources []string -} - -func (m *mockScanner) GetName() string { - return m.name -} - -func (m *mockScanner) IsEnabled(ctx context.Context, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (bool, error) { - return m.enabled, m.enabledError -} - -func (m *mockScanner) ScanNamespace(ctx context.Context, namespace string, clientset kubernetes.Interface, dynamicClient dynamic.Interface) (*ResourceReferences, error) { - return m.scanResult, m.scanError -} - -func (m *mockScanner) GetSupportedResources() []string { - return m.supportedResources -} - -func TestNewScannerRegistry(t *testing.T) { - registry := NewScannerRegistry() - if registry == nil { - t.Error("expected non-nil registry") - } - if registry.scanners == nil { - t.Error("expected non-nil scanners slice") - } - if len(registry.scanners) != 0 { - t.Errorf("expected empty scanners slice, got %d scanners", len(registry.scanners)) - } -} - -func TestScannerRegistry_RegisterScanner(t *testing.T) { - registry := NewScannerRegistry() - - scanner1 := &mockScanner{name: "scanner1"} - scanner2 := &mockScanner{name: "scanner2"} - - registry.RegisterScanner(scanner1) - if len(registry.scanners) != 1 { - t.Errorf("expected 1 scanner, got %d", len(registry.scanners)) - } - - registry.RegisterScanner(scanner2) - if len(registry.scanners) != 2 { - t.Errorf("expected 2 scanners, got %d", len(registry.scanners)) - } - - // Verify the scanners are properly registered - if registry.scanners[0].GetName() != "scanner1" { - t.Errorf("expected first scanner name 'scanner1', got %q", registry.scanners[0].GetName()) - } - if registry.scanners[1].GetName() != "scanner2" { - t.Errorf("expected second scanner name 'scanner2', got %q", registry.scanners[1].GetName()) - } -} - -func TestScannerRegistry_ScanNamespace(t *testing.T) { - registry := NewScannerRegistry() - clientset := fake.NewSimpleClientset() - - // Create mock scanners - scanner1 := &mockScanner{ - name: "enabled-scanner", - enabled: true, - scanResult: &ResourceReferences{ - ConfigMaps: []string{"config1", "config2"}, - Secrets: []string{"secret1"}, - PVCs: []string{}, - }, - } - - scanner2 := &mockScanner{ - name: "disabled-scanner", - enabled: false, - scanResult: &ResourceReferences{ - ConfigMaps: []string{"should-not-appear"}, - Secrets: []string{"should-not-appear"}, - PVCs: []string{"should-not-appear"}, - }, - } - - scanner3 := &mockScanner{ - name: "another-enabled-scanner", - enabled: true, - scanResult: &ResourceReferences{ - ConfigMaps: []string{"config3"}, - Secrets: []string{"secret2", "secret3"}, - PVCs: []string{"pvc1"}, - }, - } - - registry.RegisterScanner(scanner1) - registry.RegisterScanner(scanner2) - registry.RegisterScanner(scanner3) - - refs, err := registry.ScanNamespace(context.TODO(), "test-namespace", clientset, nil) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - expectedConfigMaps := []string{"config1", "config2", "config3"} - expectedSecrets := []string{"secret1", "secret2", "secret3"} - expectedPVCs := []string{"pvc1"} - - // 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", expected) - } - } - - // 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", expected) - } - } - - // 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", expected) - } - } - - // Verify disabled scanner results are not included - for _, configMap := range refs.ConfigMaps { - if configMap == "should-not-appear" { - t.Error("disabled scanner results should not be included") - } - } -} - -func TestScannerRegistry_GetEnabledScanners(t *testing.T) { - registry := NewScannerRegistry() - clientset := fake.NewSimpleClientset() - - scanner1 := &mockScanner{name: "enabled1", enabled: true} - scanner2 := &mockScanner{name: "disabled1", enabled: false} - scanner3 := &mockScanner{name: "enabled2", enabled: true} - scanner4 := &mockScanner{name: "disabled2", enabled: false} - - registry.RegisterScanner(scanner1) - registry.RegisterScanner(scanner2) - registry.RegisterScanner(scanner3) - registry.RegisterScanner(scanner4) - - enabled, err := registry.GetEnabledScanners(context.TODO(), clientset, nil) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - if len(enabled) != 2 { - t.Errorf("expected 2 enabled scanners, got %d", len(enabled)) - return - } - - // Check that only enabled scanners are returned - enabledNames := make(map[string]bool) - for _, scanner := range enabled { - enabledNames[scanner.GetName()] = true - } - - if !enabledNames["enabled1"] { - t.Error("expected 'enabled1' scanner to be in enabled list") - } - if !enabledNames["enabled2"] { - t.Error("expected 'enabled2' scanner to be in enabled list") - } - if enabledNames["disabled1"] { - t.Error("'disabled1' scanner should not be in enabled list") - } - if enabledNames["disabled2"] { - t.Error("'disabled2' scanner should not be in enabled list") - } -} - -func TestGetGlobalRegistry(t *testing.T) { - // Test that GetGlobalRegistry returns the same instance - registry1 := GetGlobalRegistry() - registry2 := GetGlobalRegistry() - - if registry1 != registry2 { - t.Error("GetGlobalRegistry should return the same instance (singleton pattern)") - } - - // Test that the registry is properly initialized with default scanners - if len(registry1.scanners) == 0 { - t.Error("expected global registry to have default scanners registered") - } - - // Check that WorkflowTemplate scanner is registered - found := false - for _, scanner := range registry1.scanners { - if scanner.GetName() == "Argo Workflows WorkflowTemplate" { - found = true - break - } - } - if !found { - t.Error("expected WorkflowTemplate scanner to be registered in global registry") - } -} 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 eb77ec8b..67d33682 100644 --- a/pkg/kor/pvc.go +++ b/pkg/kor/pvc.go @@ -8,15 +8,15 @@ 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" "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" - "github.com/yonahd/kor/pkg/kor/externaldeps" ) -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) @@ -39,19 +39,15 @@ func retrieveUsedPvcs(clientset kubernetes.Interface, namespace string) ([]strin return usedPvcs, err } -func retrieveUsedPvcsFromExternalCRDs(clientset kubernetes.Interface, namespace string) ([]string, error) { - registry := externaldeps.GetGlobalRegistry() - dynamicClient := GetDynamicClient("") - - refs, err := registry.ScanNamespace(context.TODO(), namespace, clientset, dynamicClient) +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 RemoveDuplicatesAndSort(refs.PVCs), nil + return refs.PVCs, nil } -func processNamespacePvcs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options, opts common.Opts) ([]ResourceInfo, error) { +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 @@ -77,19 +73,20 @@ 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 external CRDs (like Argo WorkflowTemplates) - externalPvcs, err := retrieveUsedPvcsFromExternalCRDs(clientset, namespace) + // Retrieve PVCs referenced by Argo WorkflowTemplates + argoPvcs, err := retrieveUsedPvcsFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) if err != nil { return nil, err } // Combine all used PVCs - allUsedPvcs := append(usedPvcs, externalPvcs...) + allUsedPvcs := append(podPvcs, argoPvcs...) allUsedPvcs = RemoveDuplicatesAndSort(allUsedPvcs) var diff []ResourceInfo @@ -111,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 aa1e7de1..b2be1d60 100644 --- a/pkg/kor/secrets.go +++ b/pkg/kor/secrets.go @@ -9,13 +9,13 @@ 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" "github.com/yonahd/kor/pkg/common" "github.com/yonahd/kor/pkg/filters" - "github.com/yonahd/kor/pkg/kor/externaldeps" ) var exceptionSecretTypes = []string{ @@ -46,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 @@ -115,16 +115,12 @@ func retrieveUsedSecret(clientset kubernetes.Interface, namespace string) ([]str return envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, nil } -func retrieveUsedSecretsFromExternalCRDs(clientset kubernetes.Interface, namespace string) ([]string, error) { - registry := externaldeps.GetGlobalRegistry() - dynamicClient := GetDynamicClient("") - - refs, err := registry.ScanNamespace(context.TODO(), namespace, clientset, dynamicClient) +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 RemoveDuplicatesAndSort(refs.Secrets), nil + return refs.Secrets, nil } func retrieveSecretNames(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, []string, error) { @@ -171,14 +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 external CRDs (like Argo WorkflowTemplates) - externalSecrets, err := retrieveUsedSecretsFromExternalCRDs(clientset, namespace) + // Retrieve Secrets referenced by Argo WorkflowTemplates + argoSecrets, err := retrieveUsedSecretFromArgoWorkflowTemplates(clientset, dynamicClient, namespace) if err != nil { return nil, err } @@ -189,7 +186,7 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, fi initContainerEnvSecrets = RemoveDuplicatesAndSort(initContainerEnvSecrets) pullSecrets = RemoveDuplicatesAndSort(pullSecrets) tlsSecrets = RemoveDuplicatesAndSort(tlsSecrets) - externalSecrets = RemoveDuplicatesAndSort(externalSecrets) + argoSecrets = RemoveDuplicatesAndSort(argoSecrets) secretNames, unusedSecretNames, err := retrieveSecretNames(clientset, namespace, filterOpts) if err != nil { @@ -204,7 +201,7 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, fi pullSecrets, tlsSecrets, initContainerEnvSecrets, - externalSecrets, + argoSecrets, } for _, slice := range slicesToAppend { @@ -232,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) }