Files
capsule/pkg/runtime/client/ignore_test.go
Oliver Bähler a6b830b1af feat: add ruleset api(#1844)
* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(config): remove usergroups default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(config): remove usergroups default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* sec(ghsa-2ww6-hf35-mfjm): intercept namespace subresource

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2026-01-27 14:28:48 +01:00

425 lines
10 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package client_test
import (
"reflect"
"testing"
"github.com/fluxcd/pkg/apis/kustomize"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/projectcapsule/capsule/pkg/runtime/client"
)
func TestIgnoreRule_Matches(t *testing.T) {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion("apps/v1")
obj.SetKind("Deployment")
obj.SetNamespace("ns1")
obj.SetName("my-deploy")
obj.SetLabels(map[string]string{"app": "demo"})
obj.SetAnnotations(map[string]string{"a": "b"})
t.Run("nil receiver matches all", func(t *testing.T) {
var r *client.IgnoreRule
if !r.Matches(obj) {
t.Fatalf("expected true")
}
})
t.Run("nil target matches all", func(t *testing.T) {
r := &client.IgnoreRule{Paths: []string{"/x"}, Target: nil}
if !r.Matches(obj) {
t.Fatalf("expected true")
}
})
t.Run("matches by kind/name/namespace", func(t *testing.T) {
r := &client.IgnoreRule{
Paths: []string{"/x"},
Target: &kustomize.Selector{
Group: "apps",
Version: "v1",
Kind: "Deployment",
Namespace: "ns1",
Name: "my-deploy",
},
}
if !r.Matches(obj) {
t.Fatalf("expected true")
}
})
t.Run("does not match when kind differs", func(t *testing.T) {
r := &client.IgnoreRule{
Paths: []string{"/x"},
Target: &kustomize.Selector{
Group: "apps",
Version: "v1",
Kind: "StatefulSet",
Namespace: "ns1",
Name: "my-deploy",
},
}
if r.Matches(obj) {
t.Fatalf("expected false")
}
})
t.Run("matches by label selector", func(t *testing.T) {
r := &client.IgnoreRule{
Paths: []string{"/x"},
Target: &kustomize.Selector{
Group: "apps",
Version: "v1",
Kind: "Deployment",
LabelSelector: "app=demo",
},
}
if !r.Matches(obj) {
t.Fatalf("expected true")
}
})
t.Run("matches by annotation selector", func(t *testing.T) {
r := &client.IgnoreRule{
Paths: []string{"/x"},
Target: &kustomize.Selector{
Group: "apps",
Version: "v1",
Kind: "Deployment",
AnnotationSelector: "a=b",
},
}
if !r.Matches(obj) {
t.Fatalf("expected true")
}
})
t.Run("invalid regex in selector returns false", func(t *testing.T) {
// jsondiff.NewSelectorRegex treats certain fields as regex; a broken one should error.
r := &client.IgnoreRule{
Paths: []string{"/x"},
Target: &kustomize.Selector{
Kind: "Deployment",
Name: "[", // invalid regex
},
}
if r.Matches(obj) {
t.Fatalf("expected false")
}
})
}
func Test_jsonPointerGet(t *testing.T) {
obj := map[string]any{
"metadata": map[string]any{
"labels": map[string]any{
"app": "demo",
"a/b": "v",
"t~k": "v2",
},
},
"spec": map[string]any{
"list": []any{
"zero",
map[string]any{"x": "y"},
},
},
}
t.Run("root empty pointer", func(t *testing.T) {
v, ok := client.JsonPointerGet(obj, "")
if !ok {
t.Fatalf("expected ok")
}
if v == nil {
t.Fatalf("expected value")
}
})
t.Run("root slash pointer", func(t *testing.T) {
v, ok := client.JsonPointerGet(obj, "/")
if !ok {
t.Fatalf("expected ok")
}
_, isMap := v.(map[string]any)
if !isMap {
t.Fatalf("expected map root")
}
})
t.Run("simple path", func(t *testing.T) {
v, ok := client.JsonPointerGet(obj, "/metadata/labels/app")
if !ok || v != "demo" {
t.Fatalf("expected demo, got ok=%v v=%v", ok, v)
}
})
t.Run("escaped slash key ~1", func(t *testing.T) {
v, ok := client.JsonPointerGet(obj, "/metadata/labels/a~1b")
if !ok || v != "v" {
t.Fatalf("expected v, got ok=%v v=%v", ok, v)
}
})
t.Run("escaped tilde key ~0", func(t *testing.T) {
v, ok := client.JsonPointerGet(obj, "/metadata/labels/t~0k")
if !ok || v != "v2" {
t.Fatalf("expected v2, got ok=%v v=%v", ok, v)
}
})
t.Run("array index", func(t *testing.T) {
v, ok := client.JsonPointerGet(obj, "/spec/list/0")
if !ok || v != "zero" {
t.Fatalf("expected zero, got ok=%v v=%v", ok, v)
}
})
t.Run("array index into object", func(t *testing.T) {
v, ok := client.JsonPointerGet(obj, "/spec/list/1/x")
if !ok || v != "y" {
t.Fatalf("expected y, got ok=%v v=%v", ok, v)
}
})
t.Run("missing path", func(t *testing.T) {
_, ok := client.JsonPointerGet(obj, "/metadata/labels/nope")
if ok {
t.Fatalf("expected not ok")
}
})
t.Run("bad array index", func(t *testing.T) {
_, ok := client.JsonPointerGet(obj, "/spec/list/nope")
if ok {
t.Fatalf("expected not ok")
}
})
t.Run("out of bounds array index", func(t *testing.T) {
_, ok := client.JsonPointerGet(obj, "/spec/list/99")
if ok {
t.Fatalf("expected not ok")
}
})
t.Run("type mismatch", func(t *testing.T) {
_, ok := client.JsonPointerGet(obj, "/metadata/labels/app/x")
if ok {
t.Fatalf("expected not ok")
}
})
}
func Test_jsonPointerSet(t *testing.T) {
t.Run("set root fails", func(t *testing.T) {
obj := map[string]any{"a": "b"}
if err := client.JsonPointerSet(obj, "", "x"); err == nil {
t.Fatalf("expected error")
}
if err := client.JsonPointerSet(obj, "/", "x"); err == nil {
t.Fatalf("expected error")
}
})
t.Run("set creates intermediate maps", func(t *testing.T) {
obj := map[string]any{}
if err := client.JsonPointerSet(obj, "/spec/template/metadata/labels/app", "demo"); err != nil {
t.Fatalf("unexpected err: %v", err)
}
v, ok := client.JsonPointerGet(obj, "/spec/template/metadata/labels/app")
if !ok || v != "demo" {
t.Fatalf("expected demo, got ok=%v v=%v", ok, v)
}
})
t.Run("set overwrites non-map intermediate with map", func(t *testing.T) {
obj := map[string]any{
"spec": "not-a-map",
}
if err := client.JsonPointerSet(obj, "/spec/x", "y"); err != nil {
t.Fatalf("unexpected err: %v", err)
}
v, ok := client.JsonPointerGet(obj, "/spec/x")
if !ok || v != "y" {
t.Fatalf("expected y, got ok=%v v=%v", ok, v)
}
})
t.Run("set supports escaped keys", func(t *testing.T) {
obj := map[string]any{}
if err := client.JsonPointerSet(obj, "/metadata/labels/a~1b", "v"); err != nil {
t.Fatalf("unexpected err: %v", err)
}
v, ok := client.JsonPointerGet(obj, "/metadata/labels/a~1b")
if !ok || v != "v" {
t.Fatalf("expected v, got ok=%v v=%v", ok, v)
}
})
}
func Test_jsonPointerDelete(t *testing.T) {
t.Run("delete root fails", func(t *testing.T) {
obj := map[string]any{"a": "b"}
if err := client.JsonPointerDelete(obj, ""); err == nil {
t.Fatalf("expected error")
}
if err := client.JsonPointerDelete(obj, "/"); err == nil {
t.Fatalf("expected error")
}
})
t.Run("delete existing leaf", func(t *testing.T) {
obj := map[string]any{
"metadata": map[string]any{
"labels": map[string]any{
"app": "demo",
},
},
}
if err := client.JsonPointerDelete(obj, "/metadata/labels/app"); err != nil {
t.Fatalf("unexpected err: %v", err)
}
_, ok := client.JsonPointerGet(obj, "/metadata/labels/app")
if ok {
t.Fatalf("expected deleted")
}
})
t.Run("delete missing path is no-op", func(t *testing.T) {
obj := map[string]any{
"metadata": map[string]any{},
}
if err := client.JsonPointerDelete(obj, "/metadata/labels/app"); err != nil {
t.Fatalf("unexpected err: %v", err)
}
})
t.Run("delete stops on non-map intermediate", func(t *testing.T) {
obj := map[string]any{
"metadata": "not-a-map",
}
if err := client.JsonPointerDelete(obj, "/metadata/labels/app"); err != nil {
t.Fatalf("unexpected err: %v", err)
}
// still unchanged
if obj["metadata"] != "not-a-map" {
t.Fatalf("expected unchanged")
}
})
}
func Test_preserveIgnoredPaths(t *testing.T) {
t.Run("copies live value into desired when present", func(t *testing.T) {
desired := map[string]any{
"metadata": map[string]any{
"labels": map[string]any{
"keep": "x",
},
},
}
live := map[string]any{
"metadata": map[string]any{
"labels": map[string]any{
"keep": "x",
"other": "y",
},
},
}
client.PreserveIgnoredPaths(desired, live, []string{"/metadata/labels/other"})
v, ok := client.JsonPointerGet(desired, "/metadata/labels/other")
if !ok || v != "y" {
t.Fatalf("expected preserved value y, got ok=%v v=%v", ok, v)
}
})
t.Run("deletes desired value when missing from live", func(t *testing.T) {
desired := map[string]any{
"metadata": map[string]any{
"labels": map[string]any{
"toDelete": "x",
},
},
}
live := map[string]any{
"metadata": map[string]any{
"labels": map[string]any{},
},
}
client.PreserveIgnoredPaths(desired, live, []string{"/metadata/labels/toDelete"})
_, ok := client.JsonPointerGet(desired, "/metadata/labels/toDelete")
if ok {
t.Fatalf("expected key to be deleted in desired")
}
})
t.Run("handles nested missing parents by creating them on set", func(t *testing.T) {
desired := map[string]any{}
live := map[string]any{
"spec": map[string]any{
"template": map[string]any{
"metadata": map[string]any{
"annotations": map[string]any{
"a": "b",
},
},
},
},
}
client.PreserveIgnoredPaths(desired, live, []string{"/spec/template/metadata/annotations/a"})
v, ok := client.JsonPointerGet(desired, "/spec/template/metadata/annotations/a")
if !ok || v != "b" {
t.Fatalf("expected b, got ok=%v v=%v", ok, v)
}
})
}
func Test_matchIgnorePaths(t *testing.T) {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion("apps/v1")
obj.SetKind("Deployment")
obj.SetNamespace("ns1")
obj.SetName("my-deploy")
obj.SetLabels(map[string]string{"app": "demo"})
rules := []client.IgnoreRule{
{
Paths: []string{"/a"},
// nil target => matches all
},
{
Paths: []string{"/b", "/c"},
Target: &kustomize.Selector{
Group: "apps",
Version: "v1",
Kind: "Deployment",
Namespace: "ns1",
Name: "my-deploy",
},
},
{
Paths: []string{"/nope"},
Target: &kustomize.Selector{
Kind: "StatefulSet",
},
},
}
out := client.MatchIgnorePaths(rules, obj)
want := []string{"/a", "/b", "/c"}
if !reflect.DeepEqual(out, want) {
t.Fatalf("unexpected paths:\nwant=%v\ngot =%v", want, out)
}
}