Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions deploy/charts/disco-agent/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,11 @@ data:
resource-type:
version: v1
resource: pods
- kind: k8s-dynamic
name: ark/configmaps
include-resources-by-labels:
conjur.org/name: "conjur-connect-configmap"
config:
resource-type:
version: v1
resource: configmaps
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions examples/machinehub.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions examples/machinehub/input.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@
"items": []
}
},
{
"data-gatherer": "ark/configmaps",
"data": {
"items": []
}
},
{
"data-gatherer": "ark/serviceaccounts",
"data": {
Expand Down
2 changes: 2 additions & 0 deletions internal/cyberark/dataupload/dataupload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions pkg/client/client_cyberark.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/client/client_cyberark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/datagatherer/k8sdynamic/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
140 changes: 135 additions & 5 deletions pkg/datagatherer/k8sdynamic/dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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.
Expand All @@ -88,9 +100,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 {
Expand All @@ -104,6 +120,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
}
Expand All @@ -115,6 +135,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")
}
Expand Down Expand Up @@ -145,6 +173,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()
},
Expand Down Expand Up @@ -219,6 +250,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
Expand Down Expand Up @@ -302,6 +337,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.
Expand Down Expand Up @@ -367,9 +409,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")
Expand Down Expand Up @@ -563,6 +619,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
Expand Down
Loading
Loading