Ctrl K
ComunidadGuías…

pkg/k8score/owner_index_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package k8score

import (
	"context"
	"errors"
	"slices"
	"testing"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	apiruntime "k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes/fake"
	k8stesting "k8s.io/client-go/testing"
)

func ownedPod(namespace, name string, owners ...metav1.OwnerReference) *corev1.Pod {
	return &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name, UID: types.UID("uid-" + name), OwnerReferences: owners}}
}

func ownerRef(kind, name, uid string) metav1.OwnerReference {
	return metav1.OwnerReference{APIVersion: "apps/v1", Kind: kind, Name: name, UID: types.UID(uid)}
}

func names[T interface{ GetName() string }](objs []T) []string {
	var out []string
	for _, o := range objs {
		out = append(out, o.GetName())
	}
	slices.Sort(out)
	return out
}

func objectNames(objs []apiruntime.Object) []string {
	var out []string
	for _, o := range objs {
		out = append(out, o.(interface{ GetName() string }).GetName())
	}
	slices.Sort(out)
	return out
}

func TestOwnerUIDsNamesEveryReferencedUID(t *testing.T) {
	pod := ownedPod("alpha", "p", ownerRef("ReplicaSet", "rs", "uid-rs"), ownerRef("Deployment", "wrong-kind-and-name", "uid-d"), ownerRef("ReplicaSet", "no-uid", ""))
	if got, _ := ownerUIDs(pod); !slices.Equal(got, []string{"uid-rs", "uid-d"}) {
		t.Fatalf("typed = %v; want every referenced UID, coordinates ignored, empty UIDs skipped", got)
	}
	u := &unstructured.Unstructured{Object: map[string]any{"metadata": map[string]any{"ownerReferences": []any{
		map[string]any{"apiVersion": "v1", "kind": "ConfigMap", "name": "c", "uid": "uid-c"},
		map[string]any{"kind": "broken"},
	}}}}
	if got, _ := ownerUIDs(u); !slices.Equal(got, []string{"uid-c"}) {
		t.Fatalf("unstructured = %v", got)
	}
	if got, _ := ownerUIDs(ownedPod("alpha", "orphan")); got != nil {
		t.Fatalf("no owners = %v, want nil", got)
	}
	if got, _ := ownerUIDs(&unstructured.Unstructured{Object: map[string]any{}}); got != nil {
		t.Fatalf("no metadata = %v, want nil", got)
	}
}

func TestOwnerUIDsAllocatesOnlyItsResult(t *testing.T) {
	pod := ownedPod("alpha", "p", ownerRef("ReplicaSet", "rs", "uid-rs"))
	orphan := ownedPod("alpha", "o")
	u := &unstructured.Unstructured{Object: map[string]any{"metadata": map[string]any{"ownerReferences": []any{
		map[string]any{"uid": "uid-rs"},
	}}}}
	for name, obj := range map[string]any{"typed": pod, "unstructured": u} {
		if allocs := testing.AllocsPerRun(100, func() { _, _ = ownerUIDs(obj) }); allocs > 1 {
			t.Errorf("%s: %v allocations per object, want at most 1", name, allocs)
		}
	}
	if allocs := testing.AllocsPerRun(100, func() { _, _ = ownerUIDs(orphan) }); allocs != 0 {
		t.Errorf("unowned object: %v allocations, want 0", allocs)
	}
}

func TestTypedReadableDependentsUsesEveryStoreItsTargetNeeds(t *testing.T) {
	rs := ownerRef("ReplicaSet", "web-1", "uid-rs")
	client := fake.NewClientset(
		ownedPod("alpha", "a1", rs),
		ownedPod("alpha", "a2", ownerRef("Deployment", "elsewhere", "uid-rs")),
		ownedPod("beta", "b1", rs),
		ownedPod("alpha", "unrelated", ownerRef("ReplicaSet", "other", "uid-other")),
		&corev1.Event{ObjectMeta: metav1.ObjectMeta{Namespace: "alpha", Name: "e", OwnerReferences: []metav1.OwnerReference{rs}}},
	)
	for name, cfg := range map[string]CacheConfig{
		"cluster-wide":         {ResourceTypes: map[string]bool{Pods: true, Events: true}},
		"partitioned by space": {ResourceScopes: map[string]ResourceScope{Pods: {Enabled: true, Namespace: "alpha"}}, ResourceScopeNamespaces: map[string][]string{Pods: {"alpha", "beta"}}},
	} {
		t.Run(name, func(t *testing.T) {
			cfg.Client = client
			rc, err := NewResourceCache(cfg)
			if err != nil {
				t.Fatal(err)
			}
			defer rc.Stop()
			all, ok := rc.ReadableDependents(Pods, nil, "uid-rs")
			if want := []string{"a1", "a2", "b1"}; !ok || !slices.Equal(objectNames(all), want) {
				t.Fatalf("whole scope = %v, %v; want %v", objectNames(all), ok, want)
			}
			alpha, ok := rc.ReadableDependents(Pods, []string{"alpha"}, "uid-rs")
			if want := []string{"a1", "a2"}; !ok || !slices.Equal(objectNames(alpha), want) {
				t.Fatalf("alpha = %v, %v; want %v", objectNames(alpha), ok, want)
			}
			if none, ok := rc.ReadableDependents(Pods, []string{}, "uid-rs"); !ok || len(none) != 0 {
				t.Fatalf("no namespaces = %v, %v; want an empty complete answer", objectNames(none), ok)
			}
			if _, ok := rc.ReadableDependents(Pods, []string{"gamma"}, "uid-rs"); name != "cluster-wide" && ok {
				t.Fatal("a namespace the collector doesn't hold must not read as complete")
			}
		})
	}
}

// A collector of one namespace holds its store under no namespace, as one
// cluster-wide informer does; a read of a namespace it doesn't collect must
// still fail rather than find nothing there.
func TestTypedReadableDependentsRefusesANamespaceOneNamespaceCollectorLacks(t *testing.T) {
	rs := ownerRef("ReplicaSet", "web-1", "uid-rs")
	client := fake.NewClientset(ownedPod("alpha", "a1", rs), ownedPod("beta", "b1", rs))
	rc, err := NewResourceCache(CacheConfig{Client: client, ResourceScopes: map[string]ResourceScope{Pods: {Enabled: true, Namespace: "alpha"}}})
	if err != nil {
		t.Fatal(err)
	}
	defer rc.Stop()
	if alpha, ok := rc.ReadableDependents(Pods, []string{"alpha"}, "uid-rs"); !ok || !slices.Equal(objectNames(alpha), []string{"a1"}) {
		t.Fatalf("alpha = %v, %v; want [a1]", objectNames(alpha), ok)
	}
	for _, namespace := range []string{"beta", "gamma"} {
		if got, ok := rc.ReadableDependents(Pods, []string{namespace}, "uid-rs"); ok {
			t.Fatalf("%s = %v, read as complete; the collector holds only alpha", namespace, objectNames(got))
		}
	}
}

func TestTypedReadableDependentsRefusesWhatItCannotAnswer(t *testing.T) {
	rs := ownerRef("ReplicaSet", "web-1", "uid-rs")
	client := fake.NewClientset(&corev1.Event{ObjectMeta: metav1.ObjectMeta{Namespace: "alpha", Name: "e", OwnerReferences: []metav1.OwnerReference{rs}}})
	rc, err := NewResourceCache(CacheConfig{Client: client, ResourceTypes: map[string]bool{Pods: true, Events: true}})
	if err != nil {
		t.Fatal(err)
	}
	defer rc.Stop()
	if _, ok := rc.ReadableDependents(Events, nil, "uid-rs"); ok {
		t.Fatal("Events aren't indexed, so they can't answer")
	}
	if _, ok := rc.ReadableDependents(Secrets, nil, "uid-rs"); ok {
		t.Fatal("a kind the cache doesn't hold can't answer")
	}
	var none *ResourceCache
	if _, ok := none.ReadableDependents(Pods, nil, "uid-rs"); ok {
		t.Fatal("a nil cache can't answer")
	}
}

func TestTypedOwnerIndexFollowsUpdatesAndDeletes(t *testing.T) {
	client := fake.NewClientset(ownedPod("alpha", "p", ownerRef("ReplicaSet", "old", "uid-old")))
	rc, err := NewResourceCache(CacheConfig{Client: client, ResourceTypes: map[string]bool{Pods: true, ReplicaSets: true}})
	if err != nil {
		t.Fatal(err)
	}
	defer rc.Stop()
	ctx := context.Background()
	moved := ownedPod("alpha", "p", ownerRef("ReplicaSet", "new", "uid-new"))
	if _, err := client.CoreV1().Pods("alpha").Update(ctx, moved, metav1.UpdateOptions{}); err != nil {
		t.Fatal(err)
	}
	waitUntil(t, 5*time.Second, "the update", func() bool {
		got, _ := rc.ReadableDependents(Pods, nil, "uid-new")
		return len(got) == 1
	})
	if old, _ := rc.ReadableDependents(Pods, nil, "uid-old"); len(old) != 0 {
		t.Fatal("an owner the object no longer names still finds it")
	}
	if err := client.CoreV1().Pods("alpha").Delete(ctx, "p", metav1.DeleteOptions{}); err != nil {
		t.Fatal(err)
	}
	waitUntil(t, 5*time.Second, "the delete", func() bool {
		got, _ := rc.ReadableDependents(Pods, nil, "uid-new")
		return len(got) == 0
	})
	if _, err := client.AppsV1().ReplicaSets("alpha").Create(ctx, &appsv1.ReplicaSet{ObjectMeta: metav1.ObjectMeta{Namespace: "alpha", Name: "rs", OwnerReferences: []metav1.OwnerReference{ownerRef("Deployment", "d", "uid-d")}}}, metav1.CreateOptions{}); err != nil {
		t.Fatal(err)
	}
	waitUntil(t, 5*time.Second, "the create", func() bool {
		got, _ := rc.ReadableDependents(ReplicaSets, []string{"alpha"}, "uid-d")
		return len(got) == 1
	})
}

func TestDynamicReadableIndexedReadsOnlyItsServingInformers(t *testing.T) {
	gvr := schema.GroupVersionResource{Group: "example.io", Version: "v1", Resource: "widgets"}
	owned := func(namespace, name, uid string) *unstructured.Unstructured {
		return &unstructured.Unstructured{Object: map[string]any{
			"apiVersion": "example.io/v1", "kind": "Widget",
			"metadata": map[string]any{"name": name, "namespace": namespace, "ownerReferences": []any{
				map[string]any{"apiVersion": "apps/v1", "kind": "Deployment", "name": "d", "uid": uid},
			}},
		}}
	}
	dyn := newWidgetDynamicClient(owned("alpha", "a", "uid-d"), owned("beta", "b", "uid-d"), owned("alpha", "other", "uid-other"))
	d, err := NewDynamicResourceCache(DynamicCacheConfig{DynamicClient: dyn})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(d.Stop)
	if _, ok := d.ReadableIndexed(gvr, "alpha", OwnerUIDIndex, "uid-d"); ok {
		t.Fatal("with no informer, nothing can answer")
	}
	if err := d.startWatching(gvr, "alpha"); err != nil {
		t.Fatal(err)
	}
	waitUntil(t, 5*time.Second, "alpha's informer", func() bool { return d.IsNamespaceSynced(gvr, "alpha") })
	got, ok := d.ReadableIndexed(gvr, "alpha", OwnerUIDIndex, "uid-d")
	if !ok || !slices.Equal(names(got), []string{"a"}) {
		t.Fatalf("alpha = %v, %v", names(got), ok)
	}
	if _, ok := d.ReadableIndexed(gvr, "", OwnerUIDIndex, "uid-d"); ok {
		t.Fatal("namespace informers can't answer for the cluster")
	}
	if err := d.startWatching(gvr, ""); err != nil {
		t.Fatal(err)
	}
	waitUntil(t, 5*time.Second, "the cluster-wide informer", func() bool { return d.IsClusterWideSynced(gvr) })
	all, ok := d.ReadableIndexed(gvr, "", OwnerUIDIndex, "uid-d")
	if !ok || !slices.Equal(names(all), []string{"a", "b"}) {
		t.Fatalf("cluster-wide = %v, %v", names(all), ok)
	}
	beta, ok := d.ReadableIndexed(gvr, "beta", OwnerUIDIndex, "uid-d")
	if !ok || !slices.Equal(names(beta), []string{"b"}) {
		t.Fatalf("beta from the cluster-wide informer = %v, %v; want beta's only", names(beta), ok)
	}
}

// The lookup itself refuses a store that hasn't loaded, for callers that
// didn't assess the kind first.
func TestTypedReadableDependentsRefusesAnUnloadedStore(t *testing.T) {
	client := fake.NewClientset()
	client.PrependReactor("list", "secrets", func(k8stesting.Action) (bool, apiruntime.Object, error) {
		return true, nil, errors.New("initial list held")
	})
	rc, err := NewResourceCache(CacheConfig{Client: client, ResourceTypes: map[string]bool{Pods: true, Secrets: true}, DeferredTypes: map[string]bool{Secrets: true}})
	if err != nil {
		t.Fatal(err)
	}
	defer rc.Stop()
	if _, ok := rc.ReadableDependents(Secrets, nil, "uid-x"); ok {
		t.Fatal("a store still loading answered for the whole kind")
	}
	if _, ok := rc.ReadableDependents(Secrets, []string{"alpha"}, "uid-x"); ok {
		t.Fatal("a store still loading answered for a namespace")
	}
	if _, ok := rc.ReadableDependents(Pods, nil, "uid-x"); !ok {
		t.Fatal("a loaded store must answer")
	}
}