From 8fc4343fb4248742176281083d1153e3d61e8c2d Mon Sep 17 00:00:00 2001 From: Atanas Chuchev Date: Mon, 12 Jan 2026 14:58:47 +0200 Subject: [PATCH 1/4] Add ConfigMaps discovery and reporting support - Add ConfigMaps field to Snapshot struct for data upload - Register ConfigMaps informer in kubernetesNativeResources - Add ark/configmaps extractor function for data processing - Update test coverage to include ConfigMaps - Add ConfigMaps configuration to disco-agent templates - Update example configurations and test snapshots This enhancement allows the agent to discover and report ConfigMap resources alongside existing resources like Pods and Daemonsets. --- .../disco-agent/templates/configmap.yaml | 6 +++++ .../__snapshot__/configmap_test.yaml.snap | 24 +++++++++++++++++++ examples/machinehub.yaml | 8 +++++++ examples/machinehub/input.json | 6 +++++ internal/cyberark/dataupload/dataupload.go | 2 ++ pkg/client/client_cyberark.go | 3 +++ pkg/client/client_cyberark_test.go | 1 + pkg/datagatherer/k8s/dynamic.go | 3 +++ 8 files changed, 53 insertions(+) diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 231a26cd..85d7d063 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -107,3 +107,9 @@ data: resource-type: version: v1 resource: pods + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + version: v1 + resource: configmaps diff --git a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap index 2c70df00..88a0c103 100644 --- a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap +++ b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap @@ -95,6 +95,12 @@ custom-cluster-description: resource-type: version: v1 resource: pods + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + version: v1 + resource: configmaps kind: ConfigMap metadata: labels: @@ -202,6 +208,12 @@ custom-cluster-name: resource-type: version: v1 resource: pods + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + version: v1 + resource: configmaps kind: ConfigMap metadata: labels: @@ -309,6 +321,12 @@ custom-period: resource-type: version: v1 resource: pods + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + version: v1 + resource: configmaps kind: ConfigMap metadata: labels: @@ -416,6 +434,12 @@ defaults: resource-type: version: v1 resource: pods + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + version: v1 + resource: configmaps kind: ConfigMap metadata: labels: diff --git a/examples/machinehub.yaml b/examples/machinehub.yaml index ea0b28e5..4b5f0282 100644 --- a/examples/machinehub.yaml +++ b/examples/machinehub.yaml @@ -125,3 +125,11 @@ data-gatherers: resource-type: version: v1 resource: pods + +# Gather Kubernetes configmaps +- name: ark/configmaps + kind: "k8s-dynamic" + config: + resource-type: + version: v1 + resource: configmaps \ No newline at end of file diff --git a/examples/machinehub/input.json b/examples/machinehub/input.json index 2cdba65c..244f39a9 100644 --- a/examples/machinehub/input.json +++ b/examples/machinehub/input.json @@ -118,6 +118,12 @@ "items": [] } }, + { + "data-gatherer": "ark/configmaps", + "data": { + "items": [] + } + }, { "data-gatherer": "ark/serviceaccounts", "data": { diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index b9ccb5f5..63502660 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -82,6 +82,8 @@ type Snapshot struct { Daemonsets []runtime.Object `json:"daemonsets"` // Pods is a list of Pod resources in the cluster. Pods []runtime.Object `json:"pods"` + // ConfigMaps is a list of ConfigMap resources in the cluster. + ConfigMaps []runtime.Object `json:"configmaps"` } // PutSnapshot PUTs the supplied snapshot to an [AWS presigned URL] which it obtains via the CyberArk inventory API. diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go index c9310265..5c0e7578 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -186,6 +186,9 @@ var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Sn "ark/pods": func(r *api.DataReading, s *dataupload.Snapshot) error { return extractResourceListFromReading(r, &s.Pods) }, + "ark/configmaps": func(r *api.DataReading, s *dataupload.Snapshot) error { + return extractResourceListFromReading(r, &s.ConfigMaps) + }, } // convertDataReadings processes a list of DataReadings using the provided diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go index f0df5c64..75a95505 100644 --- a/pkg/client/client_cyberark_test.go +++ b/pkg/client/client_cyberark_test.go @@ -89,6 +89,7 @@ var defaultDynamicDatagathererNames = []string{ "ark/statefulsets", "ark/daemonsets", "ark/pods", + "ark/configmaps", } // fakeReadings returns a set of fake readings that includes a discovery reading diff --git a/pkg/datagatherer/k8s/dynamic.go b/pkg/datagatherer/k8s/dynamic.go index 92f10c33..244c76fc 100644 --- a/pkg/datagatherer/k8s/dynamic.go +++ b/pkg/datagatherer/k8s/dynamic.go @@ -144,6 +144,9 @@ var kubernetesNativeResources = map[schema.GroupVersionResource]sharedInformerFu corev1.SchemeGroupVersion.WithResource("pods"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer { return sharedFactory.Core().V1().Pods().Informer() }, + corev1.SchemeGroupVersion.WithResource("configmaps"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer { + return sharedFactory.Core().V1().ConfigMaps().Informer() + }, corev1.SchemeGroupVersion.WithResource("nodes"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer { return sharedFactory.Core().V1().Nodes().Informer() }, From 4698ff0c6bec614ca41ffc1724fbd78f16997985 Mon Sep 17 00:00:00 2001 From: Atanas Chuchev Date: Wed, 21 Jan 2026 17:04:58 +0200 Subject: [PATCH 2/4] Add label and annotation filtering support to ConfigDynamic --- .../disco-agent/templates/configmap.yaml | 2 + pkg/datagatherer/k8s/cache.go | 2 + pkg/datagatherer/k8s/dynamic.go | 137 ++++++- pkg/datagatherer/k8s/dynamic_test.go | 384 ++++++++++++++++++ 4 files changed, 520 insertions(+), 5 deletions(-) diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 85d7d063..ed126bd3 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -109,6 +109,8 @@ data: resource: pods - kind: k8s-dynamic name: ark/configmaps + include-resources-by-labels: + conjur.org/name: "conjur-connect-configmap" config: resource-type: version: v1 diff --git a/pkg/datagatherer/k8s/cache.go b/pkg/datagatherer/k8s/cache.go index 4482f512..58f70da7 100644 --- a/pkg/datagatherer/k8s/cache.go +++ b/pkg/datagatherer/k8s/cache.go @@ -29,6 +29,8 @@ func (*realTime) now() time.Time { type cacheResource interface { GetUID() types.UID GetNamespace() string + GetLabels() map[string]string + GetAnnotations() map[string]string } func logCacheUpdateFailure(log logr.Logger, obj any, operation string) { diff --git a/pkg/datagatherer/k8s/dynamic.go b/pkg/datagatherer/k8s/dynamic.go index 244c76fc..8fc49dee 100644 --- a/pkg/datagatherer/k8s/dynamic.go +++ b/pkg/datagatherer/k8s/dynamic.go @@ -76,6 +76,18 @@ type ConfigDynamic struct { IncludeNamespaces []string `yaml:"include-namespaces"` // FieldSelectors is a list of field selectors to use when listing this resource FieldSelectors []string `yaml:"field-selectors"` + // IncludeResourcesByLabels filters to include only resources that have all of the specified labels. + // This controls which resources are collected, not which labels are included. + IncludeResourcesByLabels map[string]string `yaml:"include-resources-by-labels"` + // ExcludeResourcesByLabels filters to exclude resources that have any of the specified labels. + // This controls which resources are collected, not which labels are excluded. + ExcludeResourcesByLabels map[string]string `yaml:"exclude-resources-by-labels"` + // IncludeResourcesByAnnotations filters to include only resources that have all of the specified annotations. + // This controls which resources are collected, not which annotations are included. + IncludeResourcesByAnnotations map[string]string `yaml:"include-resources-by-annotations"` + // ExcludeResourcesByAnnotations filters to exclude resources that have any of the specified annotations. + // This controls which resources are collected, not which annotations are excluded. + ExcludeResourcesByAnnotations map[string]string `yaml:"exclude-resources-by-annotations"` } // UnmarshalYAML unmarshals the ConfigDynamic resolving GroupVersionResource. @@ -87,9 +99,13 @@ func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(any) error) error { Version string `yaml:"version"` Resource string `yaml:"resource"` } `yaml:"resource-type"` - ExcludeNamespaces []string `yaml:"exclude-namespaces"` - IncludeNamespaces []string `yaml:"include-namespaces"` - FieldSelectors []string `yaml:"field-selectors"` + ExcludeNamespaces []string `yaml:"exclude-namespaces"` + IncludeNamespaces []string `yaml:"include-namespaces"` + FieldSelectors []string `yaml:"field-selectors"` + IncludeResourcesByLabels map[string]string `yaml:"include-resources-by-labels"` + ExcludeResourcesByLabels map[string]string `yaml:"exclude-resources-by-labels"` + IncludeResourcesByAnnotations map[string]string `yaml:"include-resources-by-annotations"` + ExcludeResourcesByAnnotations map[string]string `yaml:"exclude-resources-by-annotations"` }{} err := unmarshal(&aux) if err != nil { @@ -103,6 +119,10 @@ func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(any) error) error { c.ExcludeNamespaces = aux.ExcludeNamespaces c.IncludeNamespaces = aux.IncludeNamespaces c.FieldSelectors = aux.FieldSelectors + c.IncludeResourcesByLabels = aux.IncludeResourcesByLabels + c.ExcludeResourcesByLabels = aux.ExcludeResourcesByLabels + c.IncludeResourcesByAnnotations = aux.IncludeResourcesByAnnotations + c.ExcludeResourcesByAnnotations = aux.ExcludeResourcesByAnnotations return nil } @@ -114,6 +134,14 @@ func (c *ConfigDynamic) validate() error { errs = append(errs, "cannot set excluded and included namespaces") } + if len(c.ExcludeResourcesByLabels) > 0 && len(c.IncludeResourcesByLabels) > 0 { + errs = append(errs, "cannot use both include-resources-by-labels and exclude-resources-by-labels") + } + + if len(c.ExcludeResourcesByAnnotations) > 0 && len(c.IncludeResourcesByAnnotations) > 0 { + errs = append(errs, "cannot use both include-resources-by-annotations and exclude-resources-by-annotations") + } + if c.GroupVersionResource.Resource == "" { errs = append(errs, "invalid configuration: GroupVersionResource.Resource cannot be empty") } @@ -221,6 +249,10 @@ func (c *ConfigDynamic) newDataGathererWithClient(ctx context.Context, cl dynami fieldSelector: fieldSelector.String(), namespaces: c.IncludeNamespaces, cache: dgCache, + includeLabels: c.IncludeResourcesByLabels, + excludeLabels: c.ExcludeResourcesByLabels, + includeAnnotations: c.IncludeResourcesByAnnotations, + excludeAnnotations: c.ExcludeResourcesByAnnotations, } // In order to reduce memory usage that might come from using Dynamic Informers @@ -304,6 +336,13 @@ type DataGathererDynamic struct { ExcludeAnnotKeys []*regexp.Regexp ExcludeLabelKeys []*regexp.Regexp + + // includeLabels and excludeLabels filter resources based on their labels + includeLabels map[string]string + excludeLabels map[string]string + // includeAnnotations and excludeAnnotations filter resources based on their annotations + includeAnnotations map[string]string + excludeAnnotations map[string]string } // Run starts the dynamic data gatherer's informers for resource collection. @@ -369,9 +408,23 @@ func (g *DataGathererDynamic) Fetch() (any, int, error) { cacheObject := item.Object.(*api.GatheredResource) if resource, ok := cacheObject.Resource.(cacheResource); ok { namespace := resource.GetNamespace() - if isIncludedNamespace(namespace, fetchNamespaces) { - items = append(items, cacheObject) + if !isIncludedNamespace(namespace, fetchNamespaces) { + continue + } + + // filter by labels + labels := resource.GetLabels() + if !matchesLabelFilter(labels, g.includeLabels, g.excludeLabels) { + continue + } + + // filter by annotations + annotations := resource.GetAnnotations() + if !matchesAnnotationFilter(annotations, g.includeAnnotations, g.excludeAnnotations) { + continue } + + items = append(items, cacheObject) continue } return nil, -1, fmt.Errorf("failed to parse cached resource") @@ -565,6 +618,80 @@ func isIncludedNamespace(namespace string, namespaces []string) bool { return slices.Contains(namespaces, namespace) } +// matchesLabelFilter checks if the resource labels match the include/exclude filters. +// If includeLabels is set, all key-value pairs must match for the resource to be included. +// An empty string value means "match any value for this key" (key-only matching). +// If excludeLabels is set, any matching key-value pair will exclude the resource. +func matchesLabelFilter(resourceLabels, includeLabels, excludeLabels map[string]string) bool { + // Check exclude labels first + if len(excludeLabels) > 0 { + for key, value := range excludeLabels { + if resourceValue, exists := resourceLabels[key]; exists { + // If exclude value is empty, exclude any resource with this key + // Otherwise, only exclude if the value also matches + if value == "" || resourceValue == value { + return false + } + } + } + } + + // Check include labels + if len(includeLabels) > 0 { + for key, value := range includeLabels { + resourceValue, exists := resourceLabels[key] + if !exists { + // Required label key is missing, filter it out + return false + } + // If include value is empty, we only care that the key exists + // Otherwise, the value must also match + if value != "" && resourceValue != value { + return false + } + } + } + + return true +} + +// matchesAnnotationFilter checks if the resource annotations match the include/exclude filters. +// If includeAnnotations is set, all key-value pairs must match for the resource to be included. +// An empty string value means "match any value for this key" (key-only matching). +// If excludeAnnotations is set, any matching key-value pair will exclude the resource. +func matchesAnnotationFilter(resourceAnnotations, includeAnnotations, excludeAnnotations map[string]string) bool { + // Check exclude annotations first + if len(excludeAnnotations) > 0 { + for key, value := range excludeAnnotations { + if resourceValue, exists := resourceAnnotations[key]; exists { + // If exclude value is empty, exclude any resource with this key + // Otherwise, only exclude if the value also matches + if value == "" || resourceValue == value { + return false + } + } + } + } + + // Check include annotations + if len(includeAnnotations) > 0 { + for key, value := range includeAnnotations { + resourceValue, exists := resourceAnnotations[key] + if !exists { + // Required annotation key is missing, filter it out + return false + } + // If include value is empty, we only care that the key exists + // Otherwise, the value must also match + if value != "" && resourceValue != value { + return false + } + } + } + + return true +} + func isNativeResource(gvr schema.GroupVersionResource) bool { _, ok := kubernetesNativeResources[gvr] return ok diff --git a/pkg/datagatherer/k8s/dynamic_test.go b/pkg/datagatherer/k8s/dynamic_test.go index 26b6ae90..03796fae 100644 --- a/pkg/datagatherer/k8s/dynamic_test.go +++ b/pkg/datagatherer/k8s/dynamic_test.go @@ -1264,3 +1264,387 @@ func toRegexps(keys []string) []*regexp.Regexp { } return regexps } + +func TestConfigDynamicValidate_LabelAndAnnotationFilters(t *testing.T) { + tests := []struct { + Config ConfigDynamic + ExpectedError string + }{ + { + Config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, + IncludeResourcesByLabels: map[string]string{"app": "test"}, + ExcludeResourcesByLabels: map[string]string{"env": "prod"}, + }, + ExpectedError: "cannot use both include-resources-by-labels and exclude-resources-by-labels", + }, + { + Config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, + IncludeResourcesByAnnotations: map[string]string{"app": "test"}, + ExcludeResourcesByAnnotations: map[string]string{"env": "prod"}, + }, + ExpectedError: "cannot use both include-resources-by-annotations and exclude-resources-by-annotations", + }, + { + Config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, + IncludeResourcesByLabels: map[string]string{"app": "test"}, + }, + ExpectedError: "", + }, + { + Config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "configmaps", + }, + ExcludeResourcesByLabels: map[string]string{"app": "test"}, + }, + ExpectedError: "", + }, + } + + for _, test := range tests { + err := test.Config.validate() + if err == nil && test.ExpectedError != "" { + t.Errorf("expected error: %q, got: nil", test.ExpectedError) + } + if err != nil && test.ExpectedError == "" { + t.Errorf("expected no error, got: %s", err.Error()) + } + if err != nil && test.ExpectedError != "" && !strings.Contains(err.Error(), test.ExpectedError) { + t.Errorf("expected %s, got %s", test.ExpectedError, err.Error()) + } + } +} + +func TestMatchesLabelFilter(t *testing.T) { + tests := map[string]struct { + resourceLabels map[string]string + includeLabels map[string]string + excludeLabels map[string]string + expected bool + }{ + "no filters - should match": { + resourceLabels: map[string]string{"app": "test"}, + includeLabels: nil, + excludeLabels: nil, + expected: true, + }, + "include label with exact match": { + resourceLabels: map[string]string{"app": "test", "version": "1.0"}, + includeLabels: map[string]string{"app": "test"}, + excludeLabels: nil, + expected: true, + }, + "include label key exists with empty value (key-only match)": { + resourceLabels: map[string]string{"conjur.org/name": "my-secret", "app": "test"}, + includeLabels: map[string]string{"conjur.org/name": ""}, + excludeLabels: nil, + expected: true, + }, + "include label key missing": { + resourceLabels: map[string]string{"app": "test"}, + includeLabels: map[string]string{"env": "prod"}, + excludeLabels: nil, + expected: false, + }, + "include label value mismatch": { + resourceLabels: map[string]string{"app": "test"}, + includeLabels: map[string]string{"app": "prod"}, + excludeLabels: nil, + expected: false, + }, + "exclude label with exact match": { + resourceLabels: map[string]string{"app": "test", "env": "prod"}, + includeLabels: nil, + excludeLabels: map[string]string{"env": "prod"}, + expected: false, + }, + "exclude label key exists with empty value (key-only match)": { + resourceLabels: map[string]string{"internal": "true"}, + includeLabels: nil, + excludeLabels: map[string]string{"internal": ""}, + expected: false, + }, + "exclude label key missing": { + resourceLabels: map[string]string{"app": "test"}, + includeLabels: nil, + excludeLabels: map[string]string{"env": "prod"}, + expected: true, + }, + "exclude label value mismatch": { + resourceLabels: map[string]string{"app": "test"}, + includeLabels: nil, + excludeLabels: map[string]string{"app": "prod"}, + expected: true, + }, + "multiple include labels all match": { + resourceLabels: map[string]string{"app": "test", "env": "prod", "version": "1.0"}, + includeLabels: map[string]string{"app": "test", "env": "prod"}, + excludeLabels: nil, + expected: true, + }, + "multiple include labels one missing": { + resourceLabels: map[string]string{"app": "test"}, + includeLabels: map[string]string{"app": "test", "env": "prod"}, + excludeLabels: nil, + expected: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := matchesLabelFilter(tc.resourceLabels, tc.includeLabels, tc.excludeLabels) + if result != tc.expected { + t.Errorf("expected %v, got %v", tc.expected, result) + } + }) + } +} + +func TestMatchesAnnotationFilter(t *testing.T) { + tests := map[string]struct { + resourceAnnotations map[string]string + includeAnnotations map[string]string + excludeAnnotations map[string]string + expected bool + }{ + "no filters - should match": { + resourceAnnotations: map[string]string{"description": "test"}, + includeAnnotations: nil, + excludeAnnotations: nil, + expected: true, + }, + "include annotation with exact match": { + resourceAnnotations: map[string]string{"description": "test", "owner": "team"}, + includeAnnotations: map[string]string{"description": "test"}, + excludeAnnotations: nil, + expected: true, + }, + "include annotation key exists with empty value (key-only match)": { + resourceAnnotations: map[string]string{"prometheus.io/scrape": "true"}, + includeAnnotations: map[string]string{"prometheus.io/scrape": ""}, + excludeAnnotations: nil, + expected: true, + }, + "include annotation key missing": { + resourceAnnotations: map[string]string{"description": "test"}, + includeAnnotations: map[string]string{"owner": "team"}, + excludeAnnotations: nil, + expected: false, + }, + "exclude annotation with exact match": { + resourceAnnotations: map[string]string{"description": "test", "internal": "true"}, + includeAnnotations: nil, + excludeAnnotations: map[string]string{"internal": "true"}, + expected: false, + }, + "exclude annotation key exists with empty value (key-only match)": { + resourceAnnotations: map[string]string{"deprecated": "yes"}, + includeAnnotations: nil, + excludeAnnotations: map[string]string{"deprecated": ""}, + expected: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := matchesAnnotationFilter(tc.resourceAnnotations, tc.includeAnnotations, tc.excludeAnnotations) + if result != tc.expected { + t.Errorf("expected %v, got %v", tc.expected, result) + } + }) + } +} + +func TestDynamicGatherer_Fetch_WithLabelFilters(t *testing.T) { + ctx := t.Context() + + tests := map[string]struct { + config ConfigDynamic + addObjects []runtime.Object + expected []*api.GatheredResource + expectedCount int + }{ + "include labels - key and value match for conjur.org/name": { + config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + IncludeResourcesByLabels: map[string]string{"conjur.org/name": "conjur-connect-configmap"}, + }, + addObjects: []runtime.Object{ + getObjectAnnot("test.io/v1", "TestResource", "res-with-matching-label", "default", nil, map[string]any{"conjur.org/name": "conjur-connect-configmap"}), + getObjectAnnot("test.io/v1", "TestResource", "res-with-different-value", "default", nil, map[string]any{"conjur.org/name": "other-value"}), + getObjectAnnot("test.io/v1", "TestResource", "res-without-label", "default", nil, map[string]any{"app": "test"}), + }, + expectedCount: 1, + expected: []*api.GatheredResource{ + { + Resource: getObjectAnnot("test.io/v1", "TestResource", "res-with-matching-label", "default", nil, map[string]any{"conjur.org/name": "conjur-connect-configmap"}), + }, + }, + }, + "include labels - key and value match": { + config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + IncludeResourcesByLabels: map[string]string{"app": "myapp"}, + }, + addObjects: []runtime.Object{ + getObjectAnnot("test.io/v1", "TestResource", "res-app-myapp", "default", nil, map[string]any{"app": "myapp"}), + getObjectAnnot("test.io/v1", "TestResource", "res-app-other", "default", nil, map[string]any{"app": "other"}), + }, + expectedCount: 1, + expected: []*api.GatheredResource{ + { + Resource: getObjectAnnot("test.io/v1", "TestResource", "res-app-myapp", "default", nil, map[string]any{"app": "myapp"}), + }, + }, + }, + "exclude labels - key only match": { + config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + ExcludeResourcesByLabels: map[string]string{"internal": ""}, + }, + addObjects: []runtime.Object{ + getObjectAnnot("test.io/v1", "TestResource", "res-internal", "default", nil, map[string]any{"internal": "true"}), + getObjectAnnot("test.io/v1", "TestResource", "res-public", "default", nil, map[string]any{"public": "true"}), + }, + expectedCount: 1, + expected: []*api.GatheredResource{ + { + Resource: getObjectAnnot("test.io/v1", "TestResource", "res-public", "default", nil, map[string]any{"public": "true"}), + }, + }, + }, + "exclude labels - key and value match": { + config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + ExcludeResourcesByLabels: map[string]string{"env": "test"}, + }, + addObjects: []runtime.Object{ + getObjectAnnot("test.io/v1", "TestResource", "res-env-test", "default", nil, map[string]any{"env": "test"}), + getObjectAnnot("test.io/v1", "TestResource", "res-env-prod", "default", nil, map[string]any{"env": "prod"}), + }, + expectedCount: 1, + expected: []*api.GatheredResource{ + { + Resource: getObjectAnnot("test.io/v1", "TestResource", "res-env-prod", "default", nil, map[string]any{"env": "prod"}), + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + cl := fake.NewSimpleDynamicClient(runtime.NewScheme(), tc.addObjects...) + dg, err := tc.config.newDataGathererWithClient(ctx, cl, nil) + require.NoError(t, err) + + dgd := dg.(*DataGathererDynamic) + + // Start the data gatherer + go func() { + if err = dgd.Run(ctx); err != nil { + t.Errorf("unexpected client error: %+v", err) + } + }() + + err = dgd.WaitForCacheSync(ctx) + require.NoError(t, err) + + // Give some time for the cache to populate + time.Sleep(200 * time.Millisecond) + + res, count, err := dgd.Fetch() + require.NoError(t, err) + + dynamicData := res.(*api.DynamicData) + assert.Equal(t, tc.expectedCount, count) + assert.Len(t, dynamicData.Items, tc.expectedCount) + + sortGatheredResources(dynamicData.Items) + sortGatheredResources(tc.expected) + + for i, item := range dynamicData.Items { + expectedItem := tc.expected[i] + assert.Equal(t, expectedItem.Resource.(*unstructured.Unstructured).GetName(), + item.Resource.(*unstructured.Unstructured).GetName()) + } + }) + } +} + +func TestDynamicGatherer_Fetch_WithAnnotationFilters(t *testing.T) { + ctx := t.Context() + + tests := map[string]struct { + config ConfigDynamic + addObjects []runtime.Object + expectedCount int + }{ + "include annotations - key only match": { + config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + IncludeResourcesByAnnotations: map[string]string{"prometheus.io/scrape": ""}, + }, + addObjects: []runtime.Object{ + getObjectAnnot("test.io/v1", "TestResource", "res-with-annot", "default", map[string]any{"prometheus.io/scrape": "true"}, nil), + getObjectAnnot("test.io/v1", "TestResource", "res-without-annot", "default", map[string]any{"description": "test"}, nil), + }, + expectedCount: 1, + }, + "exclude annotations - key and value match": { + config: ConfigDynamic{ + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + ExcludeResourcesByAnnotations: map[string]string{"deprecated": "true"}, + }, + addObjects: []runtime.Object{ + getObjectAnnot("test.io/v1", "TestResource", "res-deprecated", "default", map[string]any{"deprecated": "true"}, nil), + getObjectAnnot("test.io/v1", "TestResource", "res-active", "default", map[string]any{"active": "true"}, nil), + }, + expectedCount: 1, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + cl := fake.NewSimpleDynamicClient(runtime.NewScheme(), tc.addObjects...) + dg, err := tc.config.newDataGathererWithClient(ctx, cl, nil) + require.NoError(t, err) + + dgd := dg.(*DataGathererDynamic) + + // Start the data gatherer + go func() { + if err = dgd.Run(ctx); err != nil { + t.Errorf("unexpected client error: %+v", err) + } + }() + + err = dgd.WaitForCacheSync(ctx) + require.NoError(t, err) + + // Give some time for the cache to populate + time.Sleep(200 * time.Millisecond) + + _, count, err := dgd.Fetch() + require.NoError(t, err) + + assert.Equal(t, tc.expectedCount, count) + }) + } +} From a052007478ba1345484cb1d60e0300637ce4d8a3 Mon Sep 17 00:00:00 2001 From: Atanas Chuchev Date: Wed, 21 Jan 2026 17:27:51 +0200 Subject: [PATCH 3/4] Refactor label and annotation filter tests for consistency in ConfigDynamic --- pkg/datagatherer/k8sdynamic/dynamic_test.go | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/datagatherer/k8sdynamic/dynamic_test.go b/pkg/datagatherer/k8sdynamic/dynamic_test.go index 335ff431..0013fbf9 100644 --- a/pkg/datagatherer/k8sdynamic/dynamic_test.go +++ b/pkg/datagatherer/k8sdynamic/dynamic_test.go @@ -1483,8 +1483,8 @@ func TestDynamicGatherer_Fetch_WithLabelFilters(t *testing.T) { }{ "include labels - key and value match for conjur.org/name": { config: ConfigDynamic{ - GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, - IncludeResourcesByLabels: map[string]string{"conjur.org/name": "conjur-connect-configmap"}, + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + IncludeResourcesByLabels: map[string]string{"conjur.org/name": "conjur-connect-configmap"}, }, addObjects: []runtime.Object{ getObjectAnnot("test.io/v1", "TestResource", "res-with-matching-label", "default", nil, map[string]any{"conjur.org/name": "conjur-connect-configmap"}), @@ -1500,8 +1500,8 @@ func TestDynamicGatherer_Fetch_WithLabelFilters(t *testing.T) { }, "include labels - key and value match": { config: ConfigDynamic{ - GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, - IncludeResourcesByLabels: map[string]string{"app": "myapp"}, + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + IncludeResourcesByLabels: map[string]string{"app": "myapp"}, }, addObjects: []runtime.Object{ getObjectAnnot("test.io/v1", "TestResource", "res-app-myapp", "default", nil, map[string]any{"app": "myapp"}), @@ -1516,8 +1516,8 @@ func TestDynamicGatherer_Fetch_WithLabelFilters(t *testing.T) { }, "exclude labels - key only match": { config: ConfigDynamic{ - GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, - ExcludeResourcesByLabels: map[string]string{"internal": ""}, + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + ExcludeResourcesByLabels: map[string]string{"internal": ""}, }, addObjects: []runtime.Object{ getObjectAnnot("test.io/v1", "TestResource", "res-internal", "default", nil, map[string]any{"internal": "true"}), @@ -1532,8 +1532,8 @@ func TestDynamicGatherer_Fetch_WithLabelFilters(t *testing.T) { }, "exclude labels - key and value match": { config: ConfigDynamic{ - GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, - ExcludeResourcesByLabels: map[string]string{"env": "test"}, + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + ExcludeResourcesByLabels: map[string]string{"env": "test"}, }, addObjects: []runtime.Object{ getObjectAnnot("test.io/v1", "TestResource", "res-env-test", "default", nil, map[string]any{"env": "test"}), @@ -1598,8 +1598,8 @@ func TestDynamicGatherer_Fetch_WithAnnotationFilters(t *testing.T) { }{ "include annotations - key only match": { config: ConfigDynamic{ - GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, - IncludeResourcesByAnnotations: map[string]string{"prometheus.io/scrape": ""}, + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + IncludeResourcesByAnnotations: map[string]string{"prometheus.io/scrape": ""}, }, addObjects: []runtime.Object{ getObjectAnnot("test.io/v1", "TestResource", "res-with-annot", "default", map[string]any{"prometheus.io/scrape": "true"}, nil), @@ -1609,8 +1609,8 @@ func TestDynamicGatherer_Fetch_WithAnnotationFilters(t *testing.T) { }, "exclude annotations - key and value match": { config: ConfigDynamic{ - GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, - ExcludeResourcesByAnnotations: map[string]string{"deprecated": "true"}, + GroupVersionResource: schema.GroupVersionResource{Group: "test.io", Version: "v1", Resource: "testresources"}, + ExcludeResourcesByAnnotations: map[string]string{"deprecated": "true"}, }, addObjects: []runtime.Object{ getObjectAnnot("test.io/v1", "TestResource", "res-deprecated", "default", map[string]any{"deprecated": "true"}, nil), From ca6480fe7a4b495dd6c6d533bed14fdfb63a5c2b Mon Sep 17 00:00:00 2001 From: Atanas Chuchev Date: Wed, 21 Jan 2026 17:57:51 +0200 Subject: [PATCH 4/4] Add label filtering for ConfigMap resources in dynamic data gatherers --- .../tests/__snapshot__/configmap_test.yaml.snap | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap index 88a0c103..d413c197 100644 --- a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap +++ b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap @@ -97,6 +97,8 @@ custom-cluster-description: resource: pods - kind: k8s-dynamic name: ark/configmaps + include-resources-by-labels: + conjur.org/name: "conjur-connect-configmap" config: resource-type: version: v1 @@ -210,6 +212,8 @@ custom-cluster-name: resource: pods - kind: k8s-dynamic name: ark/configmaps + include-resources-by-labels: + conjur.org/name: "conjur-connect-configmap" config: resource-type: version: v1 @@ -323,6 +327,8 @@ custom-period: resource: pods - kind: k8s-dynamic name: ark/configmaps + include-resources-by-labels: + conjur.org/name: "conjur-connect-configmap" config: resource-type: version: v1 @@ -436,6 +442,8 @@ defaults: resource: pods - kind: k8s-dynamic name: ark/configmaps + include-resources-by-labels: + conjur.org/name: "conjur-connect-configmap" config: resource-type: version: v1