From 793c28afff93a7db3050bc18dda2f2d895969b6e Mon Sep 17 00:00:00 2001 From: Stefan Koelle <50440224+skoelle@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:09:25 +0200 Subject: [PATCH] Add pure secret-diff logic (kctl.DiffSecretValues) with unit tests --- internal/kctl/diff.go | 51 ++++++++++++++++++++++++++++++++ internal/kctl/diff_test.go | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 internal/kctl/diff.go create mode 100644 internal/kctl/diff_test.go diff --git a/internal/kctl/diff.go b/internal/kctl/diff.go new file mode 100644 index 0000000..afb5ecb --- /dev/null +++ b/internal/kctl/diff.go @@ -0,0 +1,51 @@ +package kctl + +import "sort" + +// SecretDiffEntry represents the comparison of one key between two secret +// sources (e.g. AWS Secrets Manager vs. a Kubernetes Secret). +type SecretDiffEntry struct { + Key string + Left string // e.g. the AWS Secrets Manager value + Right string // e.g. the decoded Kubernetes secret value + Match bool +} + +// DiffSecretValues compares two key/value maps and returns a sorted list of +// diff entries covering the union of keys present in either map. A key that +// only exists on one side is still reported, with the missing side left as +// an empty string and Match set to false (unless both sides happen to be +// empty strings). +func DiffSecretValues(left, right map[string]string) []SecretDiffEntry { + seen := map[string]bool{} + for k := range left { + seen[k] = true + } + for k := range right { + seen[k] = true + } + + keys := make([]string, 0, len(seen)) + for k := range seen { + keys = append(keys, k) + } + sort.Strings(keys) + + result := make([]SecretDiffEntry, 0, len(keys)) + for _, k := range keys { + l := left[k] + r := right[k] + result = append(result, SecretDiffEntry{Key: k, Left: l, Right: r, Match: l == r}) + } + return result +} + +// AnyMismatch reports whether at least one diff entry does not match. +func AnyMismatch(entries []SecretDiffEntry) bool { + for _, e := range entries { + if !e.Match { + return true + } + } + return false +} diff --git a/internal/kctl/diff_test.go b/internal/kctl/diff_test.go new file mode 100644 index 0000000..59aaf63 --- /dev/null +++ b/internal/kctl/diff_test.go @@ -0,0 +1,59 @@ +package kctl + +import "testing" + +func TestDiffSecretValues_AllMatch(t *testing.T) { + left := map[string]string{"a": "1", "b": "2"} + right := map[string]string{"a": "1", "b": "2"} + + entries := DiffSecretValues(left, right) + if len(entries) != 2 { + t.Fatalf("expected 2 entries, got %d", len(entries)) + } + if AnyMismatch(entries) { + t.Fatalf("expected no mismatch, got %v", entries) + } +} + +func TestDiffSecretValues_Mismatch(t *testing.T) { + left := map[string]string{"a": "1", "b": "2"} + right := map[string]string{"a": "1", "b": "different"} + + entries := DiffSecretValues(left, right) + if !AnyMismatch(entries) { + t.Fatalf("expected a mismatch, got %v", entries) + } + + var bEntry *SecretDiffEntry + for i := range entries { + if entries[i].Key == "b" { + bEntry = &entries[i] + } + } + if bEntry == nil || bEntry.Match { + t.Fatalf("expected key 'b' to be a mismatch, got %v", bEntry) + } +} + +func TestDiffSecretValues_KeyOnlyOnOneSide(t *testing.T) { + left := map[string]string{"a": "1", "only-left": "x"} + right := map[string]string{"a": "1", "only-right": "y"} + + entries := DiffSecretValues(left, right) + if len(entries) != 3 { + t.Fatalf("expected 3 entries (union of keys), got %d: %v", len(entries), entries) + } + if !AnyMismatch(entries) { + t.Fatalf("expected mismatch due to keys only present on one side") + } +} + +func TestDiffSecretValues_EmptyMaps(t *testing.T) { + entries := DiffSecretValues(nil, nil) + if len(entries) != 0 { + t.Fatalf("expected no entries for empty maps, got %v", entries) + } + if AnyMismatch(entries) { + t.Fatalf("expected no mismatch for empty maps") + } +}