Ctrl K
ComunidadGuías…

pkg/ownership/cachesource/conformance_test.go

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

Archivo original ↗Preguntar a KiBot
package cachesource

import (
	"cmp"
	"fmt"
	"reflect"
	"slices"
	"testing"

	apimeta "k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"github.com/skyhook-io/radar/pkg/ownership"
	"github.com/skyhook-io/radar/pkg/ownership/ownershiptest"
	"github.com/skyhook-io/radar/pkg/resourceid"
)

// conform checks the cache source over w against Scan of w on every question
// QueriesFor asks.
func conform(t *testing.T, w ownershiptest.World) {
	t.Helper()
	want, err := ownershiptest.Scan(w).Begin()
	if err != nil {
		t.Fatal(err)
	}
	r := render(t, w)
	before := r.storedObjects()
	got := begin(t, r.source())
	ownershiptest.Conform(t, want, got, ownershiptest.QueriesFor(w))
	if err := got.ValidateCaches(); err != nil {
		t.Fatalf("a quiet cache's session failed ValidateCaches: %v", err)
	}
	resolveAlike(t, w, want, got)
	viewsAlike(t, w, want, got)
	if after := r.storedObjects(); !reflect.DeepEqual(before, after) {
		t.Fatal("answering changed objects in the cache")
	}
}

// resolveAlike checks that ResolveOwner judges every owner reference of every
// object in w the same over both sessions: the cache source changes nothing a
// rule can see.
func resolveAlike(t *testing.T, w ownershiptest.World, want, got ownership.ReadSession) {
	t.Helper()
	for _, o := range w.Objects {
		dependent := want.Object(o.Ref)
		if dependent.State != ownership.Present {
			continue
		}
		for i, ref := range dependent.Object.OwnerReferences {
			a, b := ownership.ResolveOwner(want, dependent.Object, ref), ownership.ResolveOwner(got, dependent.Object, ref)
			if a.State != b.State || a.Ref != b.Ref || a.Gap != b.Gap || a.Owner.UID != b.Owner.UID || a.Owner.ResourceVersion != b.Owner.ResourceVersion {
				t.Errorf("%v owner %d (%s %s/%s): scan %v %v %+v, cache %v %v %+v", o.Ref, i, ref.APIVersion, ref.Kind, ref.Name, a.State, a.Ref, a.Gap, b.State, b.Ref, b.Gap)
			}
		}
	}
}

// viewsAlike checks that a View gives every answer about every object in w
// alike over both sessions, for a caller who may read everything and for one
// who may read less.
func viewsAlike(t *testing.T, w ownershiptest.World, want, got ownership.ReadSession) {
	t.Helper()
	for _, caller := range []ownership.Access{ownership.Unrestricted(), alphaOnly{}} {
		a, b := ownership.NewView(want, caller), ownership.NewView(got, caller)
		for _, o := range w.Objects {
			if x, y := fmt.Sprint(viewAnswers(a, o.Ref)), fmt.Sprint(viewAnswers(b, o.Ref)); x != y {
				t.Errorf("%v for %T:\n scan  %s\n cache %s", o.Ref, caller, x, y)
			}
		}
	}
}

// viewAnswers is what a View says about ref, without the resource versions
// and copies two sources may hold differently.
func viewAnswers(v *ownership.View, ref resourceid.Ref) []any {
	ident := func(o ownership.Observation) string { return fmt.Sprintf("%v@%s", o.Ref, o.UID) }
	resolution := func(r ownership.OwnerResolution) string {
		return fmt.Sprintf("%v %v %s %+v", r.State, r.Ref, r.Owner.UID, r.Gap)
	}
	subject, refs := v.References(ref)
	out := []any{subject.Access, subject.Read.State, subject.Read.Gap, subject.Read.Object.UID}
	for _, r := range refs {
		out = append(out, resolution(r))
	}
	owner := v.Owner(ref)
	out = append(out, owner.Reference != nil, owner.Several, resolution(owner.Resolution))
	if r := owner.Reference; r != nil {
		out = append(out, r.APIVersion, r.Kind, r.Name, r.UID, r.Controller != nil && *r.Controller)
	}
	children := v.Children(ref)
	for _, c := range children.Children {
		out = append(out, ident(c))
	}
	out = append(out, children.Gaps, children.Narrowed)
	root := v.ControllerChain(ref)
	out = append(out, root.Chain, root.Cycle, root.Truncated)
	if root.UnfollowedReference != nil {
		out = append(out, resolution(*root.UnfollowedReference))
	}
	if ownership.IsRecordingManager(ref.GroupKind()) {
		members := v.Members(ref)
		for _, m := range members.Members {
			out = append(out, ident(m))
		}
		out = append(out, members.Membership, members.Missing, members.Withheld, members.Unknown, members.Gaps)
	}
	managers := v.RecordingManagers(ref)
	for _, m := range managers.Managers {
		out = append(out, ident(m))
	}
	out = append(out, managers.Unreadable, managers.Remote, managers.Gaps)
	m := v.Manager(ref)
	out = append(out, m.State, m.Lead, fmt.Sprintf("%+v", m.Direct), m.ChainEnd, m.Narrowed)
	if m.Via != nil {
		out = append(out, fmt.Sprintf("%+v", *m.Via))
	}
	return out
}

// The cache source reads the claims an object's metadata makes as the
// oracle does, so both answer what manages it alike: a list naming it, an
// instance label inherited by a Pod, a disagreeing tracking ID, a copied one
// and a Helm release.
func TestSourceConformsOnManagerClaims(t *testing.T) {
	b := ownershiptest.NewBuilder()
	recorded := ownership.MembersRecorded
	d := b.Add("apps", "Deployment", "alpha", "web")
	rs := b.Add("apps", "ReplicaSet", "alpha", "web-1", d.OwnerRef(true))
	b.Meta(b.Add("", "Pod", "alpha", "web-1-a", rs.OwnerRef(true)), map[string]string{"argocd.argoproj.io/instance": "web"}, nil)
	cm := b.Meta(b.Add("", "ConfigMap", "alpha", "settings"), nil, map[string]string{"argocd.argoproj.io/tracking-id": "beta_old:/ConfigMap:alpha/settings"})
	b.Meta(b.Add("", "ConfigMap", "alpha", "copy"), nil, map[string]string{"argocd.argoproj.io/tracking-id": "beta_web:/ConfigMap:alpha/settings"})
	b.Meta(b.Add("", "ConfigMap", "alpha", "chart"), nil, map[string]string{"meta.helm.sh/release-name": "web", "meta.helm.sh/release-namespace": "alpha"})
	b.Manager(ownership.ArgoApplication, "beta", "web", recorded, "alpha", d.Ref, cm.Ref)
	b.Manager(ownership.ArgoApplication, "beta", "old", recorded, "alpha")
	conform(t, b.World())
}

// alphaOnly may read namespaced objects only in alpha, and no Nodes.
type alphaOnly struct{}

func (alphaOnly) CanRead(gk resourceid.GroupKind, namespace string) ownership.Decision {
	if gk == (resourceid.GroupKind{Kind: "Node"}) || namespace != "" && namespace != "alpha" {
		return ownership.Denied
	}
	return ownership.Allowed
}
func (alphaOnly) Namespaces() ([]string, bool) { return []string{"alpha"}, false }

// storedObjects deep-copies every object Radar's stores hold, by kind.
func (r *rendered) storedObjects() map[string][]runtime.Object {
	out := map[string][]runtime.Object{}
	for _, k := range ownershiptest.Kinds {
		var objs []runtime.Object
		if k.Typed {
			held, _ := r.caches.Typed.ReadableObjects(k.Resource, nil)
			for _, o := range held {
				objs = append(objs, o.DeepCopyObject())
			}
		} else {
			held, _ := r.caches.Dynamic.ListWatchedReadOnly(schema.GroupVersionResource{Group: k.Group, Version: k.Version, Resource: k.Resource})
			for _, o := range held {
				objs = append(objs, o.DeepCopyObject())
			}
		}
		slices.SortFunc(objs, func(a, b runtime.Object) int {
			am, _ := apimeta.Accessor(a)
			bm, _ := apimeta.Accessor(b)
			return cmp.Compare(am.GetNamespace()+"/"+am.GetName(), bm.GetNamespace()+"/"+bm.GetName())
		})
		out[k.GroupKind.String()] = objs
	}
	return out
}

var (
	pods, replicaSets, configMaps, jobs    = resourceid.GroupKind{Kind: "Pod"}, resourceid.GroupKind{Group: "apps", Kind: "ReplicaSet"}, resourceid.GroupKind{Kind: "ConfigMap"}, resourceid.GroupKind{Group: "batch", Kind: "Job"}
	widgetKind, gadgetKind, clusterWidgets = resourceid.GroupKind{Group: "example.io", Kind: "Widget"}, resourceid.GroupKind{Group: "example.io", Kind: "Gadget"}, resourceid.GroupKind{Group: "example.io", Kind: "ClusterWidget"}
)

func TestSourceConformsOnTheFixture(t *testing.T) {
	for name, collect := range map[string]map[resourceid.GroupKind]ownershiptest.Collection{
		"everything collected": nil,
		"some namespaces collected": {
			pods:       {Namespaces: []string{"alpha"}},
			widgetKind: {Namespaces: []string{"alpha"}, OnDemand: true},
			gadgetKind: {Namespaces: []string{"alpha", "beta"}, OnDemand: true},
		},
		"still loading": {
			replicaSets: {State: ownershiptest.Loading},
			gadgetKind:  {State: ownershiptest.Loading, OnDemand: true},
			pods:        {State: ownershiptest.Loading, Namespaces: []string{"alpha", "beta"}},
			widgetKind:  {State: ownershiptest.Loading, Namespaces: []string{"alpha"}, OnDemand: true},
		},
		"not collected or refused": {
			configMaps:     {State: ownershiptest.NotCollected},
			jobs:           {State: ownershiptest.Refused},
			widgetKind:     {State: ownershiptest.NotCollected, OnDemand: true},
			clusterWidgets: {State: ownershiptest.Refused, OnDemand: true},
		},
		"managers partly collected": {
			ownership.ArgoApplication:   {State: ownershiptest.Loading, OnDemand: true},
			ownership.FluxKustomization: {Namespaces: []string{"alpha"}, OnDemand: true},
			ownership.FluxHelmRelease:   {State: ownershiptest.Refused, OnDemand: true},
		},
		"managers not collected": {
			ownership.ArgoApplication:   {State: ownershiptest.NotCollected, OnDemand: true},
			ownership.FluxKustomization: {Namespaces: []string{"alpha", "flux-system"}, OnDemand: true},
		},
	} {
		t.Run(name, func(t *testing.T) {
			w := ownershiptest.Fixture()
			for gk, c := range collect {
				w.Collection[gk] = c
			}
			conform(t, w)
		})
	}
	t.Run("partial discovery", func(t *testing.T) {
		w := ownershiptest.Fixture()
		w.Partial = true
		conform(t, w)
	})
	t.Run("ambiguous kinds", func(t *testing.T) {
		w := ownershiptest.Fixture()
		gc := []string{"delete", "get", "list", "watch"}
		w.Resources = append(w.Resources,
			// Collected through either of two namespaced resources.
			ownership.ServedResource{Group: "example.io", Version: "v1", Kind: "Doohickey", Resource: "doohickeys", Namespaced: true, Verbs: gc},
			ownership.ServedResource{Group: "example.io", Version: "v1", Kind: "Doohickey", Resource: "moredoohickeys", Namespaced: true, Verbs: gc},
			// Collected only through its cluster-scoped resource.
			ownership.ServedResource{Group: "example.io", Version: "v1", Kind: "Whatsit", Resource: "whatsits", Namespaced: true, Verbs: []string{"get"}},
			ownership.ServedResource{Group: "example.io", Version: "v1", Kind: "Whatsit", Resource: "clusterwhatsits", Verbs: gc},
			// Served only to answer queries, as aggregated APIs do.
			ownership.ServedResource{Group: "example.io", Version: "v1", Kind: "Report", Resource: "reports", Namespaced: true},
			ownership.ServedResource{Group: "example.io", Version: "v1", Kind: "Report", Resource: "clusterreports"},
		)
		conform(t, w)
	})
	// A manager kind discovery doesn't serve is not installed, unless
	// discovery missed some groups.
	for _, partial := range []bool{false, true} {
		t.Run(fmt.Sprintf("HelmReleases not served, partial discovery %v", partial), func(t *testing.T) {
			w := ownershiptest.Fixture()
			w.Partial = partial
			w.Resources = slices.DeleteFunc(w.Resources, func(r ownership.ServedResource) bool { return r.Kind == ownership.FluxHelmRelease.Kind })
			w.Objects = slices.DeleteFunc(w.Objects, func(o ownershiptest.Object) bool { return o.Ref.GroupKind() == ownership.FluxHelmRelease })
			w.Collection[ownership.FluxHelmRelease] = ownershiptest.Collection{State: ownershiptest.NotCollected, OnDemand: true}
			conform(t, w)
		})
	}
}

func TestSourceConformsOnGeneratedWorlds(t *testing.T) {
	seeds := 40
	if testing.Short() {
		seeds = 8
	}
	for seed := range uint64(seeds) {
		t.Run(fmt.Sprint(seed), func(t *testing.T) {
			conform(t, ownershiptest.Generate(seed))
		})
	}
}