Skip to content

Commit 93a02ec

Browse files
committed
annot-exclusion: unit test RemoveTypedKeys and RemoveUnstructuredKeys
1 parent 7271bc1 commit 93a02ec

File tree

2 files changed

+273
-52
lines changed

2 files changed

+273
-52
lines changed

pkg/datagatherer/k8s/dynamic.go

+76-52
Original file line numberDiff line numberDiff line change
@@ -379,41 +379,8 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
379379
// remove managedFields from all resources
380380
Redact(RedactFields, resource)
381381

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

418385
continue
419386
}
@@ -427,23 +394,8 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
427394
item.GetObjectMeta().SetManagedFields(nil)
428395
delete(item.GetObjectMeta().GetAnnotations(), "kubectl.kubernetes.io/last-applied-configuration")
429396

430-
annots := item.GetObjectMeta().GetAnnotations()
431-
for key := range annots {
432-
for _, excludeAnnotKey := range excludeAnnotKeys {
433-
if excludeAnnotKey.MatchString(key) {
434-
delete(annots, key)
435-
}
436-
}
437-
}
438-
439-
labels := item.GetObjectMeta().GetLabels()
440-
for key := range labels {
441-
for _, excludeLabelKey := range excludeLabelKeys {
442-
if excludeLabelKey.MatchString(key) {
443-
delete(labels, key)
444-
}
445-
}
446-
}
397+
RemoveTypedKeys(excludeAnnotKeys, item.GetObjectMeta().GetAnnotations())
398+
RemoveTypedKeys(excludeLabelKeys, item.GetObjectMeta().GetLabels())
447399

448400
resource := item.(runtime.Object)
449401
gvks, _, err := scheme.Scheme.ObjectKinds(resource)
@@ -470,6 +422,78 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
470422
return nil
471423
}
472424

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

pkg/datagatherer/k8s/dynamic_test.go

+197
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)