Files
capsule/pkg/runtime/sanitize/object_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

323 lines
8.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package sanitize_test
import (
"testing"
apiMeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/projectcapsule/capsule/pkg/runtime/sanitize"
)
func TestSanitizeObject_Nil(t *testing.T) {
t.Parallel()
// Should not panic and should return nil.
if err := sanitize.SanitizeObject(nil, nil, sanitize.SanitizeOptions{
StripUID: true,
StripManagedFields: true,
StripLastApplied: true,
StripStatus: true,
}); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
}
func TestSanitizeObject_MetadataFields_TypedObject(t *testing.T) {
t.Parallel()
pod := newPodWithMeta()
opts := sanitize.SanitizeOptions{
StripUID: true,
StripManagedFields: true,
StripLastApplied: true,
StripStatus: false, // metadata-only test
}
if err := sanitize.SanitizeObject(pod, nil, opts); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
// UID stripped
if got := pod.GetUID(); got != "" {
t.Fatalf("expected UID stripped, got %q", got)
}
// ManagedFields stripped
accessor, err := apiMeta.Accessor(pod)
if err != nil {
t.Fatalf("apiMeta.Accessor failed: %v", err)
}
if mf := accessor.GetManagedFields(); len(mf) != 0 {
t.Fatalf("expected managedFields stripped, got %#v", mf)
}
// last-applied stripped, other annotation preserved
anns := pod.GetAnnotations()
if _, ok := anns["kubectl.kubernetes.io/last-applied-configuration"]; ok {
t.Fatalf("expected last-applied annotation stripped, still present: %#v", anns)
}
if anns["keep"] != "yes" {
t.Fatalf("expected other annotation preserved, got %#v", anns)
}
}
func TestSanitizeObject_LastApplied_AnnotationMapRemovedWhenEmpty(t *testing.T) {
t.Parallel()
pod := newPodWithMeta()
// Only last-applied exists.
pod.SetAnnotations(map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{"x":"y"}`,
})
opts := sanitize.SanitizeOptions{
StripLastApplied: true,
}
if err := sanitize.SanitizeObject(pod, nil, opts); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if anns := pod.GetAnnotations(); len(anns) != 0 {
t.Fatalf("expected annotations cleared (nil or empty) after removing last-applied, got %#v", anns)
}
}
func TestSanitizeObject_NoOptions_NoChanges(t *testing.T) {
t.Parallel()
pod := newPodWithMeta()
// make a copy for comparison
orig := pod.DeepCopy()
opts := sanitize.SanitizeOptions{} // everything false
if err := sanitize.SanitizeObject(pod, nil, opts); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
// Verify important bits unchanged
if pod.GetUID() != orig.GetUID() {
t.Fatalf("UID changed unexpectedly: %q -> %q", orig.GetUID(), pod.GetUID())
}
if pod.GetAnnotations()["keep"] != "yes" {
t.Fatalf("annotations changed unexpectedly: %#v", pod.GetAnnotations())
}
// ManagedFields should still be present
accessor, err := apiMeta.Accessor(pod)
if err != nil {
t.Fatalf("apiMeta.Accessor failed: %v", err)
}
origAcc, _ := apiMeta.Accessor(orig)
if len(accessor.GetManagedFields()) != len(origAcc.GetManagedFields()) {
t.Fatalf("managedFields changed unexpectedly: %#v -> %#v", origAcc.GetManagedFields(), accessor.GetManagedFields())
}
}
func TestSanitizeObject_StripStatus_TypedObject(t *testing.T) {
t.Parallel()
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatalf("AddToScheme: %v", err)
}
pod := newPodWithMeta()
// Put something into status so we can confirm its removed.
pod.Status.Phase = corev1.PodRunning
pod.Status.HostIP = "10.0.0.1"
opts := sanitize.SanitizeOptions{
StripStatus: true,
}
if err := sanitize.SanitizeObject(pod, scheme, opts); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
// After stripping status, it should be zero value.
if pod.Status.Phase != "" || pod.Status.HostIP != "" {
t.Fatalf("expected pod status stripped to zero value, got %#v", pod.Status)
}
}
func TestSanitizeObject_StripStatus_RequiresScheme(t *testing.T) {
t.Parallel()
pod := newPodWithMeta()
pod.Status.Phase = corev1.PodRunning
opts := sanitize.SanitizeOptions{
StripStatus: true,
}
if err := sanitize.SanitizeObject(pod, nil, opts); err == nil {
t.Fatalf("expected error when StripStatus=true and scheme=nil, got nil")
}
}
func TestSanitizeObject_Unstructured_FastPathMetadataAndStatus(t *testing.T) {
t.Parallel()
u := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "p",
"namespace": "ns",
"uid": "abc",
"annotations": map[string]any{
"kubectl.kubernetes.io/last-applied-configuration": `{"x":"y"}`,
"keep": "yes",
},
"managedFields": []any{
map[string]any{"manager": "x"},
},
},
"status": map[string]any{
"phase": "Running",
},
},
}
// If your SanitizeObject supports unstructured directly (recommended),
// this should work. If you keep a separate SanitizeUnstructured, then
// call that instead in this test.
opts := sanitize.SanitizeOptions{
StripUID: true,
StripManagedFields: true,
StripLastApplied: true,
StripStatus: true,
}
// scheme not needed for unstructured if your implementation detects it and uses map operations.
// If your implementation requires scheme even for unstructured, pass a scheme.
if err := sanitize.SanitizeObject(u, runtime.NewScheme(), opts); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
// Verify uid removed
if _, found, _ := unstructured.NestedFieldNoCopy(u.Object, "metadata", "uid"); found {
t.Fatalf("expected metadata.uid stripped")
}
// Verify managedFields removed
if _, found, _ := unstructured.NestedFieldNoCopy(u.Object, "metadata", "managedFields"); found {
t.Fatalf("expected metadata.managedFields stripped")
}
// Verify last-applied removed, keep preserved
anns, found, err := unstructured.NestedStringMap(u.Object, "metadata", "annotations")
if err != nil || !found {
t.Fatalf("expected annotations map to exist, err=%v found=%v", err, found)
}
if _, ok := anns["kubectl.kubernetes.io/last-applied-configuration"]; ok {
t.Fatalf("expected last-applied stripped from annotations, got %#v", anns)
}
if anns["keep"] != "yes" {
t.Fatalf("expected keep annotation preserved, got %#v", anns)
}
// Verify status removed
if _, found, _ := unstructured.NestedFieldNoCopy(u.Object, "status"); found {
t.Fatalf("expected status stripped")
}
}
func TestSanitizeObject_AllOptions_TypedObject(t *testing.T) {
t.Parallel()
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatalf("AddToScheme: %v", err)
}
pod := newPodWithMeta()
pod.Status.Phase = corev1.PodRunning
pod.Status.PodIP = "10.0.0.2"
opts := sanitize.SanitizeOptions{
StripUID: true,
StripManagedFields: true,
StripLastApplied: true,
StripStatus: true,
}
if err := sanitize.SanitizeObject(pod, scheme, opts); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
// UID stripped
if pod.GetUID() != "" {
t.Fatalf("expected UID stripped, got %q", pod.GetUID())
}
// managedFields stripped
acc, err := apiMeta.Accessor(pod)
if err != nil {
t.Fatalf("apiMeta.Accessor failed: %v", err)
}
if len(acc.GetManagedFields()) != 0 {
t.Fatalf("expected managedFields stripped, got %#v", acc.GetManagedFields())
}
// last-applied stripped
anns := pod.GetAnnotations()
if _, ok := anns["kubectl.kubernetes.io/last-applied-configuration"]; ok {
t.Fatalf("expected last-applied stripped, got %#v", anns)
}
if anns["keep"] != "yes" {
t.Fatalf("expected other annotations preserved, got %#v", anns)
}
if pod.Status.Phase != "" || pod.Status.PodIP != "" || pod.Status.HostIP != "" {
t.Fatalf("expected scalar status fields cleared, got %#v", pod.Status)
}
if len(pod.Status.Conditions) != 0 {
t.Fatalf("expected status.conditions empty, got %#v", pod.Status.Conditions)
}
if len(pod.Status.ContainerStatuses) != 0 {
t.Fatalf("expected status.containerStatuses empty, got %#v", pod.Status.ContainerStatuses)
}
}
// --- helpers ---
func newPodWithMeta() *corev1.Pod {
p := &corev1.Pod{}
p.SetName("p")
p.SetNamespace("ns")
p.SetUID(types.UID("uid-123"))
p.SetAnnotations(map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{"a":"b"}`,
"keep": "yes",
})
// ManagedFields is on ObjectMeta; easiest to set via Accessor.
// Note: type is []metav1.ManagedFieldsEntry
acc, _ := apiMeta.Accessor(p)
acc.SetManagedFields([]metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "v1",
Time: &metav1.Time{},
},
})
// Implement client.Object assertion at compile-time for sanity.
var _ client.Object = p
return p
}