Skip to content

Commit 248cf2e

Browse files
author
felix.phipps
committed
initial commit
Signed-off-by: felix.phipps <felix.phipps@cyberark.com>
1 parent 9db49f9 commit 248cf2e

2 files changed

Lines changed: 201 additions & 4 deletions

File tree

pkg/datagatherer/k8sdynamic/dynamic.go

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ type ConfigDynamic struct {
8282
FieldSelectors []string `yaml:"field-selectors"`
8383
// LabelSelectors is a list of label selectors to use when listing this resource
8484
LabelSelectors []string `yaml:"label-selectors"`
85+
// ExcludeAnnotationKeysRegex is a list of regular expressions to exclude.
86+
ExcludeAnnotationKeysRegex []string `yaml:"excludeAnnotationKeysRegex"`
87+
// ExcludeLabelKeysRegex is a list of regular expressions to exclude.
88+
ExcludeLabelKeysRegex []string `yaml:"excludeLabelKeysRegex"`
8589
}
8690

8791
// UnmarshalYAML unmarshals the ConfigDynamic resolving GroupVersionResource.
@@ -93,10 +97,12 @@ func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(any) error) error {
9397
Version string `yaml:"version"`
9498
Resource string `yaml:"resource"`
9599
} `yaml:"resource-type"`
96-
ExcludeNamespaces []string `yaml:"exclude-namespaces"`
97-
IncludeNamespaces []string `yaml:"include-namespaces"`
98-
FieldSelectors []string `yaml:"field-selectors"`
99-
LabelSelectors []string `yaml:"label-selectors"`
100+
ExcludeNamespaces []string `yaml:"exclude-namespaces"`
101+
IncludeNamespaces []string `yaml:"include-namespaces"`
102+
FieldSelectors []string `yaml:"field-selectors"`
103+
LabelSelectors []string `yaml:"label-selectors"`
104+
ExcludeAnnotationKeysRegex []string `yaml:"excludeAnnotationKeysRegex"`
105+
ExcludeLabelKeysRegex []string `yaml:"excludeLabelKeysRegex"`
100106
}{}
101107
err := unmarshal(&aux)
102108
if err != nil {
@@ -111,6 +117,8 @@ func (c *ConfigDynamic) UnmarshalYAML(unmarshal func(any) error) error {
111117
c.IncludeNamespaces = aux.IncludeNamespaces
112118
c.FieldSelectors = aux.FieldSelectors
113119
c.LabelSelectors = aux.LabelSelectors
120+
c.ExcludeAnnotationKeysRegex = aux.ExcludeAnnotationKeysRegex
121+
c.ExcludeLabelKeysRegex = aux.ExcludeLabelKeysRegex
114122

115123
return nil
116124
}
@@ -146,6 +154,18 @@ func (c *ConfigDynamic) validate() error {
146154
}
147155
}
148156

157+
for i, r := range c.ExcludeAnnotationKeysRegex {
158+
if _, err := regexp.Compile(r); err != nil {
159+
errs = append(errs, fmt.Sprintf("invalid excludeAnnotationKeysRegex[%d]: %s", i, err))
160+
}
161+
}
162+
163+
for i, r := range c.ExcludeLabelKeysRegex {
164+
if _, err := regexp.Compile(r); err != nil {
165+
errs = append(errs, fmt.Sprintf("invalid excludeLabelKeysRegex[%d]: %s", i, err))
166+
}
167+
}
168+
149169
if len(errs) > 0 {
150170
return errors.New(strings.Join(errs, ", "))
151171
}
@@ -309,6 +329,13 @@ func (c *ConfigDynamic) newDataGathererWithClient(ctx context.Context, cl dynami
309329
}
310330
newDataGatherer.registration = registration
311331

332+
for _, r := range c.ExcludeAnnotationKeysRegex {
333+
newDataGatherer.ExcludeAnnotKeys = append(newDataGatherer.ExcludeAnnotKeys, regexp.MustCompile(r))
334+
}
335+
for _, r := range c.ExcludeLabelKeysRegex {
336+
newDataGatherer.ExcludeLabelKeys = append(newDataGatherer.ExcludeLabelKeys, regexp.MustCompile(r))
337+
}
338+
312339
return newDataGatherer, nil
313340
}
314341

@@ -423,6 +450,8 @@ func (g *DataGathererDynamic) Fetch(ctx context.Context) (any, int, error) {
423450
return nil, -1, fmt.Errorf("failed to parse cached resource")
424451
}
425452

453+
items = g.excludeResources(items)
454+
426455
// Redact Secret data (which may include encrypting it if enabled)
427456
err := g.redactList(ctx, items)
428457
if err != nil {
@@ -434,6 +463,41 @@ func (g *DataGathererDynamic) Fetch(ctx context.Context) (any, int, error) {
434463
}, len(items), nil
435464
}
436465

466+
// excludeResources drops any resource whose annotation or label keys match the
467+
// configured exclusion patterns. This is distinct from redactList, which strips
468+
// matching keys from kept resources.
469+
func (g *DataGathererDynamic) excludeResources(list []*api.GatheredResource) []*api.GatheredResource {
470+
if len(g.ExcludeAnnotKeys) == 0 && len(g.ExcludeLabelKeys) == 0 {
471+
return list
472+
}
473+
result := list[:0]
474+
for _, item := range list {
475+
if !g.resourceMatchesExclusionKeys(item) {
476+
result = append(result, item)
477+
}
478+
}
479+
return result
480+
}
481+
482+
func (g *DataGathererDynamic) resourceMatchesExclusionKeys(item *api.GatheredResource) bool {
483+
if res, ok := item.Resource.(*unstructured.Unstructured); ok {
484+
return anyKeyMatches(res.GetAnnotations(), g.ExcludeAnnotKeys) ||
485+
anyKeyMatches(res.GetLabels(), g.ExcludeLabelKeys)
486+
}
487+
return false
488+
}
489+
490+
func anyKeyMatches(m map[string]string, patterns []*regexp.Regexp) bool {
491+
for key := range m {
492+
for _, p := range patterns {
493+
if p.MatchString(key) {
494+
return true
495+
}
496+
}
497+
}
498+
return false
499+
}
500+
437501
// redactList removes sensitive and superfluous data from the supplied resource list.
438502
// All resources have superfluous managed-data fields removed.
439503
// All resources have sensitive labels and annotations removed.

pkg/datagatherer/k8sdynamic/dynamic_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,34 @@ label-selectors:
300300
t.Errorf("LabelSelectors does not match: got=%+v want=%+v", got, want)
301301
}
302302
}
303+
func TestUnmarshalDynamicConfig_ExclusionRegex(t *testing.T) {
304+
// Verify that the per-gatherer excludeAnnotationKeysRegex and
305+
// excludeLabelKeysRegex fields are parsed from YAML.
306+
textCfg := `
307+
resource-type:
308+
version: v1
309+
resource: secrets
310+
excludeAnnotationKeysRegex:
311+
- '^openshift\.io.*$'
312+
- '^kapp\.k14s\.io/.*$'
313+
excludeLabelKeysRegex:
314+
- '^company\.com/employee-id$'
315+
`
316+
cfg := ConfigDynamic{}
317+
if err := yaml.Unmarshal([]byte(textCfg), &cfg); err != nil {
318+
t.Fatalf("unexpected error: %+v", err)
319+
}
320+
321+
expectedAnnot := []string{`^openshift\.io.*$`, `^kapp\.k14s\.io/.*$`}
322+
expectedLabel := []string{`^company\.com/employee-id$`}
323+
324+
if got, expected := cfg.ExcludeAnnotationKeysRegex, expectedAnnot; !reflect.DeepEqual(got, expected) {
325+
t.Errorf("ExcludeAnnotationKeysRegex: got=%v want=%v", got, expected)
326+
}
327+
if got, expected := cfg.ExcludeLabelKeysRegex, expectedLabel; !reflect.DeepEqual(got, expected) {
328+
t.Errorf("ExcludeLabelKeysRegex: got=%v want=%v", got, expected)
329+
}
330+
}
303331

304332
func TestConfigDynamicValidate(t *testing.T) {
305333
tests := []struct {
@@ -345,6 +373,20 @@ func TestConfigDynamicValidate(t *testing.T) {
345373
},
346374
ExpectedError: "invalid field selector 0: invalid selector: 'foo'; can't understand 'foo'",
347375
},
376+
{
377+
Config: ConfigDynamic{
378+
GroupVersionResource: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
379+
ExcludeAnnotationKeysRegex: []string{`^[0-9$`},
380+
},
381+
ExpectedError: "invalid excludeAnnotationKeysRegex[0]",
382+
},
383+
{
384+
Config: ConfigDynamic{
385+
GroupVersionResource: schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
386+
ExcludeLabelKeysRegex: []string{`^[0-9$`},
387+
},
388+
ExpectedError: "invalid excludeLabelKeysRegex[0]",
389+
},
348390
}
349391

350392
for _, test := range tests {
@@ -763,6 +805,48 @@ func TestDynamicGatherer_Fetch(t *testing.T) {
763805
map[string]any{"prod": "true"},
764806
)}},
765807
},
808+
"per-gatherer excludeAnnotationKeysRegex excludes matching resources entirely": {
809+
// Resources annotated with openshift.io/* should not appear in the
810+
// output at all, not just have those keys stripped.
811+
config: ConfigDynamic{
812+
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
813+
ExcludeAnnotationKeysRegex: []string{`^openshift\.io.*$`},
814+
},
815+
addObjects: []*unstructured.Unstructured{
816+
getObjectAnnot("v1", "Secret", "excluded", "ns",
817+
map[string]any{"openshift.io/discovery": "ignore", "other": "kept"},
818+
map[string]any{},
819+
),
820+
getObjectAnnot("v1", "Secret", "included", "ns",
821+
map[string]any{"other": "kept"},
822+
map[string]any{},
823+
),
824+
},
825+
expected: []*api.GatheredResource{{Resource: getObjectAnnot("v1", "Secret", "included", "ns",
826+
map[string]any{"other": "kept"},
827+
map[string]any{},
828+
)}},
829+
},
830+
"per-gatherer excludeLabelKeysRegex excludes matching resources entirely": {
831+
config: ConfigDynamic{
832+
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
833+
ExcludeLabelKeysRegex: []string{`^discovery\.venafi\.com/exclude$`},
834+
},
835+
addObjects: []*unstructured.Unstructured{
836+
getObjectAnnot("v1", "Secret", "excluded", "ns",
837+
map[string]any{},
838+
map[string]any{"discovery.venafi.com/exclude": "true", "other": "kept"},
839+
),
840+
getObjectAnnot("v1", "Secret", "included", "ns",
841+
map[string]any{},
842+
map[string]any{"other": "kept"},
843+
),
844+
},
845+
expected: []*api.GatheredResource{{Resource: getObjectAnnot("v1", "Secret", "included", "ns",
846+
map[string]any{},
847+
map[string]any{"other": "kept"},
848+
)}},
849+
},
766850
}
767851

768852
for name, tc := range tests {
@@ -967,6 +1051,55 @@ func compareEncryptedData(t *testing.T, privKey *stdrsa.PrivateKey, got *unstruc
9671051
unstructured.RemoveNestedField(got.Object, encryptedDataFieldName)
9681052
}
9691053

1054+
// TestExcludeAnnotKeys_ExcludesResourcesFromUpload verifies that resources
1055+
// whose annotation keys match ExcludeAnnotKeys are dropped entirely from
1056+
// Fetch() results, not just have those keys stripped.
1057+
func TestExcludeAnnotKeys_ExcludesResourcesFromUpload(t *testing.T) {
1058+
ctx := t.Context()
1059+
1060+
gvrToListKind := map[schema.GroupVersionResource]string{
1061+
{Group: "", Version: "v1", Resource: "secrets"}: "UnstructuredList",
1062+
}
1063+
1064+
// "excluded" has a matching annotation key; "included" does not.
1065+
excluded := getObjectAnnot("v1", "Secret", "excluded", "ns",
1066+
map[string]any{"openshift.io/discovery": "ignore"},
1067+
map[string]any{},
1068+
)
1069+
included := getObjectAnnot("v1", "Secret", "included", "ns",
1070+
map[string]any{"other": "kept"},
1071+
map[string]any{},
1072+
)
1073+
1074+
cl := fake.NewSimpleDynamicClientWithCustomListKinds(
1075+
runtime.NewScheme(), gvrToListKind, excluded, included,
1076+
)
1077+
1078+
cfg := ConfigDynamic{
1079+
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
1080+
}
1081+
dg, err := cfg.newDataGathererWithClient(ctx, cl, nil)
1082+
require.NoError(t, err)
1083+
1084+
dgd := dg.(*DataGathererDynamic)
1085+
dgd.ExcludeAnnotKeys = []*regexp.Regexp{regexp.MustCompile(`^openshift\.io/.*$`)}
1086+
1087+
go func() { _ = dg.Run(ctx) }()
1088+
require.NoError(t, dgd.WaitForCacheSync(ctx))
1089+
1090+
res, count, err := dg.Fetch(ctx)
1091+
require.NoError(t, err)
1092+
1093+
data, ok := res.(*api.DynamicData)
1094+
require.True(t, ok)
1095+
1096+
assert.Equal(t, 1, count, "only the non-matching resource should be returned")
1097+
if assert.Len(t, data.Items, 1) {
1098+
got := data.Items[0].Resource.(*unstructured.Unstructured)
1099+
assert.Equal(t, "included", got.GetName(), "the resource with matching annotation key should be excluded")
1100+
}
1101+
}
1102+
9701103
func TestDynamicGathererNativeResources_Fetch(t *testing.T) {
9711104
// start a k8s client
9721105
// init the datagatherer's informer with the client

0 commit comments

Comments
 (0)