pkg/ownership/cachesource/conformance_test.go
Rama main · réplica de lectura del repositorio original.
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))
})
}
}