Skip to content

Commit

Permalink
annot-exclusion: unit test RemoveTypedKeys and RemoveUnstructuredKeys
Browse files Browse the repository at this point in the history
  • Loading branch information
maelvls committed Nov 14, 2024
1 parent 7271bc1 commit 93a02ec
Show file tree
Hide file tree
Showing 2 changed files with 273 additions and 52 deletions.
128 changes: 76 additions & 52 deletions pkg/datagatherer/k8s/dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,41 +379,8 @@ func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys
// remove managedFields from all resources
Redact(RedactFields, resource)

annotsRaw, ok, err := unstructured.NestedFieldNoCopy(resource.Object, "metadata", "annotations")
if err != nil {
return fmt.Errorf("wasn't able to find the metadata.annotations field: %w", err)
}
if ok {
annots, ok := annotsRaw.(map[string]interface{})
if !ok {
return fmt.Errorf("metadata.annotations isn't a map on the resource %s in namespace %s: %w", resource.GetName(), resource.GetNamespace(), err)
}
for key := range annots {
for _, excludeAnnotKey := range excludeAnnotKeys {
if excludeAnnotKey.MatchString(key) {
delete(annots, key)
}
}
}
}

labelsRaw, ok, err := unstructured.NestedFieldNoCopy(resource.Object, "metadata", "labels")
if err != nil {
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)
}
if ok {
labels, ok := labelsRaw.(map[string]interface{})
if !ok {
return fmt.Errorf("metadata.labels isn't a map on the resource %s in namespace %s: %w", resource.GetName(), resource.GetNamespace(), err)
}
for key := range labels {
for _, excludeLabelKey := range excludeLabelKeys {
if excludeLabelKey.MatchString(key) {
delete(labels, key)
}
}
}
}
RemoveUnstructuredKeys(excludeAnnotKeys, resource, "metadata", "annotations")
RemoveUnstructuredKeys(excludeLabelKeys, resource, "metadata", "labels")

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

annots := item.GetObjectMeta().GetAnnotations()
for key := range annots {
for _, excludeAnnotKey := range excludeAnnotKeys {
if excludeAnnotKey.MatchString(key) {
delete(annots, key)
}
}
}

labels := item.GetObjectMeta().GetLabels()
for key := range labels {
for _, excludeLabelKey := range excludeLabelKeys {
if excludeLabelKey.MatchString(key) {
delete(labels, key)
}
}
}
RemoveTypedKeys(excludeAnnotKeys, item.GetObjectMeta().GetAnnotations())
RemoveTypedKeys(excludeLabelKeys, item.GetObjectMeta().GetLabels())

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

// Meant for typed clientset objects.
func RemoveTypedKeys(excludeAnnotKeys []*regexp.Regexp, m map[string]string) {
for key := range m {
for _, excludeAnnotKey := range excludeAnnotKeys {
if excludeAnnotKey.MatchString(key) {
delete(m, key)
}
}
}
}

// Meant for unstructured clientset objects. Removes the keys from the field
// given as input. For example, let's say we have the following object:
//
// {
// "metadata": {
// "annotations": {
// "key1": "value1",
// "key2": "value2"
// }
// }
// }
//
// Then, the following call:
//
// RemoveUnstructuredKeys("^key1$", obj, "metadata", "annotations")
//
// Will result in:
//
// {
// "metadata": {
// "annotations": {"key2": "value2"}
// }
// }
//
// If the given path doesn't exist or leads to a non-map object, nothing
// happens. The leaf object must either be a map[string]interface{} (that's
// what's returned by the unstructured clientset) or a map[string]string (that's
// what's returned by the typed clientset).
func RemoveUnstructuredKeys(excludeKeys []*regexp.Regexp, obj *unstructured.Unstructured, path ...string) {
annotsRaw, ok, err := unstructured.NestedFieldNoCopy(obj.Object, path...)
if err != nil {
return
}
if !ok {
return
}

// The field may be nil since yaml.Unmarshal's omitempty might not be set on
// on this struct field.
if annotsRaw == nil {
return
}

// The only possible type in an unstructured.Unstructured object is
// map[string]interface{}. That's because the yaml.Unmarshal func is used
// with an empty map[string]interface{} object, which means all nested
// objects will be unmarshalled to a map[string]interface{}.
annots, ok := annotsRaw.(map[string]interface{})
if !ok {
return
}

for key := range annots {
for _, excludeAnnotKey := range excludeKeys {
if excludeAnnotKey.MatchString(key) {
delete(annots, key)
}
}
}
}

// generateExcludedNamespacesFieldSelector creates a field selector string from
// a list of namespaces to exclude.
func generateExcludedNamespacesFieldSelector(excludeNamespaces []string) fields.Selector {
Expand Down
197 changes: 197 additions & 0 deletions pkg/datagatherer/k8s/dynamic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1086,3 +1086,200 @@ func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
return true
}
}

func TestRemoveUnstructuredKeys(t *testing.T) {
t.Run("remove single key", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenExclude: []string{"^key1$"},
givenObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
"annotations": map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
},
expectObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
},
},
}))

t.Run("remove keys using multiple regexes", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenExclude: []string{"^key1$", "^key2$"},
givenObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
"annotations": map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
},
expectObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
},
},
}))

t.Run("remove multiple keys with a single regex", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenExclude: []string{"key.*"},
givenObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
"annotations": map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
},
expectObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
},
},
}))

t.Run("with no regex, the object is untouched", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenExclude: []string{},
givenObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
"annotations": map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
},
expectObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
"annotations": map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
},
}))

// The "leaf" field is the field that is at the end of the path. For
// example, "annotations" is the leaf field in metadata.annotations.
t.Run("works when the leaf field is not found", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenExclude: []string{},

givenObj: map[string]interface{}{"metadata": map[string]interface{}{}},
expectObj: map[string]interface{}{"metadata": map[string]interface{}{}},
}))

t.Run("works when the leaf field is nil", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenExclude: []string{},
givenObj: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "foo",
"annotations": nil,
},
},
expectObj: map[string]interface{}{"metadata": map[string]interface{}{"name": "foo"}},
}))

t.Run("works when leaf field is unexpectedly not nil and not a known map", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenObj: map[string]interface{}{"metadata": map[string]interface{}{"annotations": 42}},
expectObj: map[string]interface{}{"metadata": map[string]interface{}{"annotations": 42}},
}))

// The "intermediate" field is the field that is not at the end of the path.
// For example, "metadata" is the intermediate field in
// metadata.annotations.
t.Run("works when the intermediate field doesn't exist", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenObj: map[string]interface{}{},
expectObj: map[string]interface{}{},
}))

t.Run("works when the intermediate field is nil", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenObj: map[string]interface{}{"metadata": nil},
}))

t.Run("works when the intermediate field is unexpectedly not nil and not a map", run_TestRemoveUnstructuredKeys(tc_RemoveUnstructuredKeys{
givenPath: []string{"metadata", "annotations"},
givenObj: map[string]interface{}{"metadata": 42},
expectObj: map[string]interface{}{"metadata": 42},
}))
}

type tc_RemoveUnstructuredKeys struct {
givenExclude []string
givenObj map[string]interface{}
givenPath []string
expectObj map[string]interface{}
}

func run_TestRemoveUnstructuredKeys(tc tc_RemoveUnstructuredKeys) func(*testing.T) {
return func(t *testing.T) {
t.Helper()
RemoveUnstructuredKeys(toRegexps(tc.givenExclude), &unstructured.Unstructured{Object: tc.givenObj}, tc.givenPath...)
}
}

func TestRemoveTypedKeys(t *testing.T) {
t.Run("remove single key", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
givenExclude: []string{"^key1$"},
given: map[string]string{"key1": "value1", "key2": "value2"},
expected: map[string]string{"key2": "value2"},
}))

t.Run("remove keys using multiple regexes", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
givenExclude: []string{"^key1$", "^key2$"},
given: map[string]string{"key1": "value1", "key2": "value2"},
expected: map[string]string{},
}))

t.Run("remove multiple keys with a single regex", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
givenExclude: []string{"key.*"},
given: map[string]string{"key1": "value1", "key2": "value2"},
expected: map[string]string{},
}))

t.Run("with no regex, the object is untouched", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
givenExclude: []string{},
given: map[string]string{"key1": "value1", "key2": "value2"},
expected: map[string]string{"key1": "value1", "key2": "value2"},
}))

t.Run("works when the map is nil", run_TestRemoveTypedKeys(tc_TestRemoveTypedKeys{
givenExclude: []string{"^key1$"},
given: nil,
expected: nil,
}))
}

type tc_TestRemoveTypedKeys struct {
givenExclude []string
given map[string]string
expected map[string]string
}

func run_TestRemoveTypedKeys(tc tc_TestRemoveTypedKeys) func(t *testing.T) {
return func(t *testing.T) {
RemoveTypedKeys(toRegexps(tc.givenExclude), tc.given)
assert.Equal(t, tc.expected, tc.given)
}
}

func toRegexps(keys []string) []*regexp.Regexp {
var regexps []*regexp.Regexp
for _, key := range keys {
regexps = append(regexps, regexp.MustCompile(key))
}
return regexps
}

0 comments on commit 93a02ec

Please sign in to comment.