Skip to content

Commit 120453f

Browse files
committed
annot-exclusion: unit test RemoveTypedKeys and RemoveUnstructuredKeys
1 parent 563ae77 commit 120453f

File tree

2 files changed

+273
-52
lines changed

2 files changed

+273
-52
lines changed

pkg/datagatherer/k8s/dynamic.go

Lines changed: 76 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -376,41 +376,8 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
376376
// remove managedFields from all resources
377377
Redact(RedactFields, resource)
378378

379-
annotsRaw, ok, err := unstructured.NestedFieldNoCopy(resource.Object, "metadata", "annotations")
380-
if err != nil {
381-
return fmt.Errorf("wasn't able to find the metadata.annotations field: %w", err)
382-
}
383-
if ok {
384-
annots, ok := annotsRaw.(map[string]interface{})
385-
if !ok {
386-
return fmt.Errorf("metadata.annotations isn't a map on the resource %s in namespace %s: %w", resource.GetName(), resource.GetNamespace(), err)
387-
}
388-
for key := range annots {
389-
for _, excludeAnnotKey := range excludeAnnotKeys {
390-
if excludeAnnotKey.MatchString(key) {
391-
delete(annots, key)
392-
}
393-
}
394-
}
395-
}
396-
397-
labelsRaw, ok, err := unstructured.NestedFieldNoCopy(resource.Object, "metadata", "labels")
398-
if err != nil {
399-
return fmt.Errorf("wasn't able to find the metadata.labels field for the resource %s in namespace %s: %w", resource.GetName(), resource.GetNamespace(), err)
400-
}
401-
if ok {
402-
labels, ok := labelsRaw.(map[string]interface{})
403-
if !ok {
404-
return fmt.Errorf("metadata.labels isn't a map on the resource %s in namespace %s: %w", resource.GetName(), resource.GetNamespace(), err)
405-
}
406-
for key := range labels {
407-
for _, excludeLabelKey := range excludeLabelKeys {
408-
if excludeLabelKey.MatchString(key) {
409-
delete(labels, key)
410-
}
411-
}
412-
}
413-
}
379+
RemoveUnstructuredKeys(excludeAnnotKeys, resource, "metadata", "annotations")
380+
RemoveUnstructuredKeys(excludeLabelKeys, resource, "metadata", "labels")
414381

415382
continue
416383
}
@@ -424,23 +391,8 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
424391
item.GetObjectMeta().SetManagedFields(nil)
425392
delete(item.GetObjectMeta().GetAnnotations(), "kubectl.kubernetes.io/last-applied-configuration")
426393

427-
annots := item.GetObjectMeta().GetAnnotations()
428-
for key := range annots {
429-
for _, excludeAnnotKey := range excludeAnnotKeys {
430-
if excludeAnnotKey.MatchString(key) {
431-
delete(annots, key)
432-
}
433-
}
434-
}
435-
436-
labels := item.GetObjectMeta().GetLabels()
437-
for key := range labels {
438-
for _, excludeLabelKey := range excludeLabelKeys {
439-
if excludeLabelKey.MatchString(key) {
440-
delete(labels, key)
441-
}
442-
}
443-
}
394+
RemoveTypedKeys(excludeAnnotKeys, item.GetObjectMeta().GetAnnotations())
395+
RemoveTypedKeys(excludeLabelKeys, item.GetObjectMeta().GetLabels())
444396

445397
resource := item.(runtime.Object)
446398
gvks, _, err := scheme.Scheme.ObjectKinds(resource)
@@ -467,6 +419,78 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
467419
return nil
468420
}
469421

422+
// Meant for typed clientset objects.
423+
func RemoveTypedKeys(excludeAnnotKeys []*regexp.Regexp, m map[string]string) {
424+
for key := range m {
425+
for _, excludeAnnotKey := range excludeAnnotKeys {
426+
if excludeAnnotKey.MatchString(key) {
427+
delete(m, key)
428+
}
429+
}
430+
}
431+
}
432+
433+
// Meant for unstructured clientset objects. Removes the keys from the field
434+
// given as input. For example, let's say we have the following object:
435+
//
436+
// {
437+
// "metadata": {
438+
// "annotations": {
439+
// "key1": "value1",
440+
// "key2": "value2"
441+
// }
442+
// }
443+
// }
444+
//
445+
// Then, the following call:
446+
//
447+
// RemoveUnstructuredKeys("^key1$", obj, "metadata", "annotations")
448+
//
449+
// Will result in:
450+
//
451+
// {
452+
// "metadata": {
453+
// "annotations": {"key2": "value2"}
454+
// }
455+
// }
456+
//
457+
// If the given path doesn't exist or leads to a non-map object, nothing
458+
// happens. The leaf object must either be a map[string]interface{} (that's
459+
// what's returned by the unstructured clientset) or a map[string]string (that's
460+
// what's returned by the typed clientset).
461+
func RemoveUnstructuredKeys(excludeKeys []*regexp.Regexp, obj *unstructured.Unstructured, path ...string) {
462+
annotsRaw, ok, err := unstructured.NestedFieldNoCopy(obj.Object, path...)
463+
if err != nil {
464+
return
465+
}
466+
if !ok {
467+
return
468+
}
469+
470+
// The field may be nil since yaml.Unmarshal's omitempty might not be set on
471+
// on this struct field.
472+
if annotsRaw == nil {
473+
return
474+
}
475+
476+
// The only possible type in an unstructured.Unstructured object is
477+
// map[string]interface{}. That's because the yaml.Unmarshal func is used
478+
// with an empty map[string]interface{} object, which means all nested
479+
// objects will be unmarshalled to a map[string]interface{}.
480+
annots, ok := annotsRaw.(map[string]interface{})
481+
if !ok {
482+
return
483+
}
484+
485+
for key := range annots {
486+
for _, excludeAnnotKey := range excludeKeys {
487+
if excludeAnnotKey.MatchString(key) {
488+
delete(annots, key)
489+
}
490+
}
491+
}
492+
}
493+
470494
// generateExcludedNamespacesFieldSelector creates a field selector string from
471495
// a list of namespaces to exclude.
472496
func generateExcludedNamespacesFieldSelector(excludeNamespaces []string) fields.Selector {

pkg/datagatherer/k8s/dynamic_test.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,3 +1086,200 @@ func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
10861086
return true
10871087
}
10881088
}
1089+
1090+
func TestRemoveUnstructuredKeys(t *testing.T) {
1091+
t.Run("remove single key", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1092+
givenPath: []string{"metadata", "annotations"},
1093+
givenExclude: []string{"^key1$"},
1094+
givenObj: map[string]interface{}{
1095+
"metadata": map[string]interface{}{
1096+
"name": "foo",
1097+
"annotations": map[string]interface{}{
1098+
"key1": "value1",
1099+
"key2": "value2",
1100+
},
1101+
},
1102+
},
1103+
expectObj: map[string]interface{}{
1104+
"metadata": map[string]interface{}{
1105+
"name": "foo",
1106+
},
1107+
},
1108+
}))
1109+
1110+
t.Run("remove keys using multiple regexes", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1111+
givenPath: []string{"metadata", "annotations"},
1112+
givenExclude: []string{"^key1$", "^key2$"},
1113+
givenObj: map[string]interface{}{
1114+
"metadata": map[string]interface{}{
1115+
"name": "foo",
1116+
"annotations": map[string]interface{}{
1117+
"key1": "value1",
1118+
"key2": "value2",
1119+
},
1120+
},
1121+
},
1122+
expectObj: map[string]interface{}{
1123+
"metadata": map[string]interface{}{
1124+
"name": "foo",
1125+
},
1126+
},
1127+
}))
1128+
1129+
t.Run("remove multiple keys with a single regex", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1130+
givenPath: []string{"metadata", "annotations"},
1131+
givenExclude: []string{"key.*"},
1132+
givenObj: map[string]interface{}{
1133+
"metadata": map[string]interface{}{
1134+
"name": "foo",
1135+
"annotations": map[string]interface{}{
1136+
"key1": "value1",
1137+
"key2": "value2",
1138+
},
1139+
},
1140+
},
1141+
expectObj: map[string]interface{}{
1142+
"metadata": map[string]interface{}{
1143+
"name": "foo",
1144+
},
1145+
},
1146+
}))
1147+
1148+
t.Run("with no regex, the object is untouched", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1149+
givenPath: []string{"metadata", "annotations"},
1150+
givenExclude: []string{},
1151+
givenObj: map[string]interface{}{
1152+
"metadata": map[string]interface{}{
1153+
"name": "foo",
1154+
"annotations": map[string]interface{}{
1155+
"key1": "value1",
1156+
"key2": "value2",
1157+
},
1158+
},
1159+
},
1160+
expectObj: map[string]interface{}{
1161+
"metadata": map[string]interface{}{
1162+
"name": "foo",
1163+
"annotations": map[string]interface{}{
1164+
"key1": "value1",
1165+
"key2": "value2",
1166+
},
1167+
},
1168+
},
1169+
}))
1170+
1171+
// The "leaf" field is the field that is at the end of the path. For
1172+
// example, "annotations" is the leaf field in metadata.annotations.
1173+
t.Run("works when the leaf field is not found", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1174+
givenPath: []string{"metadata", "annotations"},
1175+
givenExclude: []string{},
1176+
1177+
givenObj: map[string]interface{}{"metadata": map[string]interface{}{}},
1178+
expectObj: map[string]interface{}{"metadata": map[string]interface{}{}},
1179+
}))
1180+
1181+
t.Run("works when the leaf field is nil", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1182+
givenPath: []string{"metadata", "annotations"},
1183+
givenExclude: []string{},
1184+
givenObj: map[string]interface{}{
1185+
"metadata": map[string]interface{}{
1186+
"name": "foo",
1187+
"annotations": nil,
1188+
},
1189+
},
1190+
expectObj: map[string]interface{}{"metadata": map[string]interface{}{"name": "foo"}},
1191+
}))
1192+
1193+
t.Run("works when leaf field is unexpectedly not nil and not a known map", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1194+
givenPath: []string{"metadata", "annotations"},
1195+
givenObj: map[string]interface{}{"metadata": map[string]interface{}{"annotations": 42}},
1196+
expectObj: map[string]interface{}{"metadata": map[string]interface{}{"annotations": 42}},
1197+
}))
1198+
1199+
// The "intermediate" field is the field that is not at the end of the path.
1200+
// For example, "metadata" is the intermediate field in
1201+
// metadata.annotations.
1202+
t.Run("works when the intermediate field doesn't exist", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1203+
givenPath: []string{"metadata", "annotations"},
1204+
givenObj: map[string]interface{}{},
1205+
expectObj: map[string]interface{}{},
1206+
}))
1207+
1208+
t.Run("works when the intermediate field is nil", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1209+
givenPath: []string{"metadata", "annotations"},
1210+
givenObj: map[string]interface{}{"metadata": nil},
1211+
}))
1212+
1213+
t.Run("works when the intermediate field is unexpectedly not nil and not a map", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
1214+
givenPath: []string{"metadata", "annotations"},
1215+
givenObj: map[string]interface{}{"metadata": 42},
1216+
expectObj: map[string]interface{}{"metadata": 42},
1217+
}))
1218+
}
1219+
1220+
type tc_RemoveUnstructuredKeys struct {
1221+
givenExclude []string
1222+
givenObj map[string]interface{}
1223+
givenPath []string
1224+
expectObj map[string]interface{}
1225+
}
1226+
1227+
func run_TestRemoveUnstructuredKeys(tc tc_RemoveUnstructuredKeys) func(*testing.T) {
1228+
return func(t *testing.T) {
1229+
t.Helper()
1230+
RemoveUnstructuredKeys(toRegexps(tc.givenExclude), &unstructured.Unstructured{Object: tc.givenObj}, tc.givenPath...)
1231+
}
1232+
}
1233+
1234+
func TestRemoveTypedKeys(t *testing.T) {
1235+
t.Run("remove single key", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
1236+
givenExclude: []string{"^key1$"},
1237+
given: map[string]string{"key1": "value1", "key2": "value2"},
1238+
expected: map[string]string{"key2": "value2"},
1239+
}))
1240+
1241+
t.Run("remove keys using multiple regexes", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
1242+
givenExclude: []string{"^key1$", "^key2$"},
1243+
given: map[string]string{"key1": "value1", "key2": "value2"},
1244+
expected: map[string]string{},
1245+
}))
1246+
1247+
t.Run("remove multiple keys with a single regex", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
1248+
givenExclude: []string{"key.*"},
1249+
given: map[string]string{"key1": "value1", "key2": "value2"},
1250+
expected: map[string]string{},
1251+
}))
1252+
1253+
t.Run("with no regex, the object is untouched", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
1254+
givenExclude: []string{},
1255+
given: map[string]string{"key1": "value1", "key2": "value2"},
1256+
expected: map[string]string{"key1": "value1", "key2": "value2"},
1257+
}))
1258+
1259+
t.Run("works when the map is nil", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
1260+
givenExclude: []string{"^key1$"},
1261+
given: nil,
1262+
expected: nil,
1263+
}))
1264+
}
1265+
1266+
type tc_TestRemoveTypedKeys struct {
1267+
givenExclude []string
1268+
given map[string]string
1269+
expected map[string]string
1270+
}
1271+
1272+
func run_TestRemoveTypedKeys(tc tc_TestRemoveTypedKeys) func(t *testing.T) {
1273+
return func(t *testing.T) {
1274+
RemoveTypedKeys(toRegexps(tc.givenExclude), tc.given)
1275+
assert.Equal(t, tc.expected, tc.given)
1276+
}
1277+
}
1278+
1279+
func toRegexps(keys []string) []*regexp.Regexp {
1280+
var regexps []*regexp.Regexp
1281+
for _, key := range keys {
1282+
regexps = append(regexps, regexp.MustCompile(key))
1283+
}
1284+
return regexps
1285+
}

0 commit comments

Comments
 (0)