pkg/ownership/view_test.go
Rama main · réplica de lectura del repositorio original.
package ownership_test
import (
"fmt"
"slices"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/skyhook-io/radar/pkg/ownership"
"github.com/skyhook-io/radar/pkg/ownership/ownershiptest"
"github.com/skyhook-io/radar/pkg/readreason"
"github.com/skyhook-io/radar/pkg/resourceid"
)
// access is a caller who may read the namespaces listed (every one when nil)
// and every kind but those denied or undecided.
type access struct {
namespaces []string
denied []resourceid.GroupKind
undecided []resourceid.GroupKind
}
func (a access) CanRead(gk resourceid.GroupKind, namespace string) ownership.Decision {
switch {
case slices.Contains(a.undecided, gk):
return ownership.Undecided
case slices.Contains(a.denied, gk), namespace != "" && a.namespaces != nil && !slices.Contains(a.namespaces, namespace):
return ownership.Denied
}
return ownership.Allowed
}
func (a access) Namespaces() ([]string, bool) { return a.namespaces, a.namespaces == nil }
func view(t *testing.T, w ownershiptest.World, a ownership.Access) *ownership.View {
t.Helper()
s, err := ownershiptest.Scan(w).Begin()
if err != nil {
t.Fatal(err)
}
return ownership.NewView(s, a)
}
func at(group, kind, namespace, name string) resourceid.Ref {
return resourceid.Ref{Group: group, Kind: kind, Namespace: namespace, Name: name}
}
var (
replicaSets = resourceid.GroupKind{Group: "apps", Kind: "ReplicaSet"}
pods = resourceid.GroupKind{Kind: "Pod"}
configMaps = resourceid.GroupKind{Kind: "ConfigMap"}
)
func TestViewOwnerIsTheControllerOrTheOnlyReference(t *testing.T) {
w := ownershiptest.Fixture()
rs := at("apps", "ReplicaSet", "alpha", "web-1")
for _, tc := range []struct {
name string
access ownership.Access
subject resourceid.Ref
index int
state ownership.OwnerState
ref resourceid.Ref
several bool
gap ownership.Gap
}{
{"a controller", ownership.Unrestricted(), at("", "Pod", "alpha", "web-1-a"), 0, ownership.OwnerFound, rs, false, ownership.Gap{}},
{"the controller of two", ownership.Unrestricted(), at("", "Pod", "alpha", "web-1-b"), 0, ownership.OwnerFound, rs, false, ownership.Gap{}},
{"several without a controller", ownership.Unrestricted(), at("", "ConfigMap", "alpha", "shared"), -1, 0, resourceid.Ref{}, true, ownership.Gap{}},
{"none", ownership.Unrestricted(), at("apps", "Deployment", "alpha", "web"), -1, 0, resourceid.Ref{}, false, ownership.Gap{}},
{"an only reference to a later incarnation", ownership.Unrestricted(), at("", "Pod", "alpha", "replaced"), 0, ownership.OwnerReplaced, at("apps", "ReplicaSet", "alpha", "web-old"), false, ownership.Gap{}},
{"an only reference to nothing", ownership.Unrestricted(), at("", "Pod", "alpha", "dangling"), 0, ownership.OwnerGone, at("apps", "ReplicaSet", "alpha", "gone"), false, ownership.Gap{}},
{"a namespaced owner of a cluster-scoped object", ownership.Unrestricted(), at("", "Node", "", "n2"), 0, ownership.OwnerUnresolvable, resourceid.Ref{}, false, ownership.Gap{}},
{"a withheld controller never gives way to the ConfigMap", access{denied: []resourceid.GroupKind{replicaSets}}, at("", "Pod", "alpha", "web-1-b"), 0, ownership.OwnerWithheld, rs, false, ownership.Gap{}},
{"an undecided check", access{undecided: []resourceid.GroupKind{replicaSets}}, at("", "Pod", "alpha", "web-1-b"), 0, ownership.OwnerUnknown, rs, false,
ownership.Gap{Kind: replicaSets, Namespace: "alpha", Reason: readreason.UserPermissionCheckFailed}},
} {
t.Run(tc.name, func(t *testing.T) {
got := view(t, w, tc.access).Owner(tc.subject)
if !got.Subject.Present() {
t.Fatalf("subject = %v %v", got.Subject.Access, got.Subject.Read.State)
}
if index := referenceIndex(got); index != tc.index || got.Several != tc.several || got.Resolution.State != tc.state || got.Resolution.Ref != tc.ref || got.Resolution.Gap != tc.gap {
t.Fatalf("Owner = index %d several %v %v %v %+v; want index %d several %v %v %v %+v",
index, got.Several, got.Resolution.State, got.Resolution.Ref, got.Resolution.Gap, tc.index, tc.several, tc.state, tc.ref, tc.gap)
}
})
}
}
func TestViewSaysNothingOfAnObjectTheCallerMayNotRead(t *testing.T) {
v := view(t, ownershiptest.Fixture(), access{denied: []resourceid.GroupKind{pods}})
pod := at("", "Pod", "alpha", "web-1-a")
if got := v.Owner(pod); got.Subject.Access != ownership.Denied || got.Subject.Read.State != ownership.Unavailable || got.Subject.Read.Object.UID != "" || got.Reference != nil {
t.Fatalf("Owner of a Pod the caller may not read = %+v", got)
}
if got := v.Children(pod); got.Subject.Present() || got.Children != nil || got.Gaps != nil || got.Complete() {
t.Fatalf("Children = %+v complete=%v", got, got.Complete())
}
unsure := view(t, ownershiptest.Fixture(), access{undecided: []resourceid.GroupKind{pods}})
if got := unsure.Children(pod); got.Subject.Present() || got.Complete() {
t.Fatalf("Children when the caller's access couldn't be checked = %+v complete=%v", got, got.Complete())
}
if got := v.ControllerChain(pod); got.Subject.Present() || len(got.Chain) != 0 || got.UnfollowedReference != nil {
t.Fatalf("ControllerChain = %+v", got)
}
}
// An object the caller fetched from the API answers for itself when Radar
// doesn't collect its kind; its owners are still read from the caches.
func TestViewSeedsAnObjectTheCachesDoNotHold(t *testing.T) {
w := ownershiptest.Fixture()
w.Collection[configMaps] = ownershiptest.Collection{State: ownershiptest.NotCollected}
fetched := at("", "ConfigMap", "alpha", "versioned")
var o ownershiptest.Object
for _, x := range w.Objects {
if x.Ref == fetched {
o = x
}
}
observation := ownership.Observation{Ref: o.Ref, UID: o.UID, OwnerReferences: o.OwnerReferences}
v := view(t, w, ownership.Unrestricted())
if got := v.Owner(fetched); got.Subject.Present() {
t.Fatalf("scenario: ConfigMaps must be uncollected, got %v", got.Subject.Read.State)
}
v = view(t, w, ownership.Unrestricted())
v.Seed(observation)
if got := v.Owner(fetched); got.Resolution.State != ownership.OwnerFound || got.Resolution.Ref != at("example.io", "Widget", "alpha", "w1") {
t.Fatalf("Owner of the fetched ConfigMap = %v %v", got.Resolution.State, got.Resolution.Ref)
}
// A read that holds the object stands.
held := view(t, ownershiptest.Fixture(), ownership.Unrestricted())
stale := observation
stale.UID = "uid-of-another-incarnation"
held.Seed(stale)
if got := held.Owner(fetched); got.Subject.Read.Object.UID != o.UID {
t.Fatalf("a seed replaced the held read: %v", got.Subject.Read.Object.UID)
}
// An Event stays excluded.
event := ownership.Observation{Ref: at("", "Event", "alpha", "e1"), UID: "uid-alpha-e1"}
events := view(t, ownershiptest.Fixture(), ownership.Unrestricted())
events.Seed(event)
if got := events.Owner(event.Ref); got.Subject.Read.State != ownership.Excluded {
t.Fatalf("a seeded Event reads %v, want excluded", got.Subject.Read.State)
}
// A seeded manager whose kind Radar doesn't collect has no list to read.
managers := ownershiptest.Fixture()
managers.Collection[ownership.ArgoApplication] = ownershiptest.Collection{State: ownershiptest.NotCollected, OnDemand: true}
app := at("argoproj.io", "Application", "argocd", "web")
seeded := view(t, managers, ownership.Unrestricted())
seeded.Seed(ownership.Observation{Ref: app, UID: "uid-argocd-web"})
if got := seeded.Members(app); got.Membership != ownership.MembersUnreadable || len(got.Gaps) != 1 || got.Gaps[0].Reason != readreason.RadarKindNotCollected {
t.Fatalf("members of an uncollected manager = %v gaps %+v; want unreadable, as Radar doesn't collect it", got.Membership, got.Gaps)
}
// Seeding grants no access.
denied := view(t, w, access{denied: []resourceid.GroupKind{configMaps}})
denied.Seed(observation)
if got := denied.Owner(fetched); got.Subject.Present() {
t.Fatal("a seed let the caller read an object it may not read")
}
}
// referenceIndex is where the chosen reference sits among the subject's own
// owner references, or -1 when none was chosen or it isn't one of them.
func referenceIndex(a ownership.OwnerAnswer) int {
refs := a.Subject.Read.Object.OwnerReferences
for i := range refs {
if a.Reference == &refs[i] {
return i
}
}
return -1
}
func childRefs(a ownership.ChildrenAnswer) []string {
var out []string
for _, c := range a.Children {
out = append(out, c.Ref.String())
}
return out
}
func TestViewChildrenResolveToTheOwnersCurrentIncarnation(t *testing.T) {
w := ownershiptest.Fixture()
unlistedWidget := []ownership.Gap{{Kind: resourceid.GroupKind{Group: "example.io", Kind: "Widget"}, Reason: readreason.KindUnknown}}
for _, tc := range []struct {
name string
access ownership.Access
owner resourceid.Ref
want []string
narrow bool
// gaps are the references to the owner Radar can't resolve.
gaps []ownership.Gap
}{
// The stray Pod in beta names the ReplicaSet's UID but resolves in its
// own namespace; wrong-coordinates names the UID under another kind.
{"a ReplicaSet's Pods", ownership.Unrestricted(), at("apps", "ReplicaSet", "alpha", "web-1"),
[]string{"Pod alpha/web-1-a", "Pod alpha/web-1-b"}, false, nil},
{"a controlled and a plain child", ownership.Unrestricted(), at("apps", "Deployment", "alpha", "web"),
[]string{"ReplicaSet.apps alpha/web-1", "Widget.example.io alpha/w1"}, false, nil},
// s1's kind can't be watched; n2 is cluster-scoped, which a namespaced
// owner can't own. j1 names a version discovery doesn't list, which the
// collector retries, so j1 may yet be a child.
{"through any served version", ownership.Unrestricted(), at("example.io", "Widget", "alpha", "w1"),
[]string{"ConfigMap alpha/shared", "ConfigMap alpha/versioned"}, false, unlistedWidget},
{"none for an earlier incarnation's dependent", ownership.Unrestricted(), at("apps", "ReplicaSet", "alpha", "web-old"), nil, false, nil},
{"none from an Event", ownership.Unrestricted(), at("", "Pod", "alpha", "web-1-a"), nil, false, nil},
{"a cluster-scoped owner's, anywhere", ownership.Unrestricted(), at("", "Node", "", "n1"),
[]string{"ClusterWidget.example.io cw1", "Gadget.example.io beta/g1"}, false, nil},
{"only those the caller may read", access{namespaces: []string{"alpha"}}, at("", "Node", "", "n1"),
[]string{"ClusterWidget.example.io cw1"}, true, nil},
{"none of a kind the caller may not read", access{denied: []resourceid.GroupKind{configMaps}}, at("example.io", "Widget", "alpha", "w1"), nil, true, unlistedWidget},
} {
t.Run(tc.name, func(t *testing.T) {
got := view(t, w, tc.access).Children(tc.owner)
if !got.Subject.Present() || !slices.Equal(got.Gaps, tc.gaps) || got.Complete() != (len(tc.gaps) == 0) {
t.Fatalf("Children = %+v; want gaps %v", got, tc.gaps)
}
if refs := childRefs(got); !slices.Equal(refs, tc.want) || got.Narrowed != tc.narrow {
t.Fatalf("Children = %q narrowed=%v; want %q narrowed=%v", refs, got.Narrowed, tc.want, tc.narrow)
}
})
}
}
func TestViewChildrenReportWhatTheSearchCouldNotRead(t *testing.T) {
w := ownershiptest.Fixture()
w.Collection[pods] = ownershiptest.Collection{State: ownershiptest.Loading}
got := view(t, w, ownership.Unrestricted()).Children(at("apps", "ReplicaSet", "alpha", "web-1"))
// One store serves every namespace, so its gap names none.
want := []ownership.Gap{{Kind: pods, Reason: readreason.KindSyncPending}}
if len(got.Children) != 0 || !slices.Equal(got.Gaps, want) || got.Complete() {
t.Fatalf("Children while Pods load = %q gaps %+v; want none and %+v", childRefs(got), got.Gaps, want)
}
// A caller who may not read Pods hears nothing of Radar's Pod stores.
got = view(t, w, access{denied: []resourceid.GroupKind{pods}}).Children(at("apps", "ReplicaSet", "alpha", "web-1"))
if len(got.Gaps) != 0 || !got.Narrowed {
t.Fatalf("Children for a caller denied Pods = gaps %+v narrowed=%v", got.Gaps, got.Narrowed)
}
}
func TestViewControllerChain(t *testing.T) {
w := ownershiptest.Fixture()
stray := at("", "Pod", "beta", "stray")
for _, tc := range []struct {
name string
access ownership.Access
start resourceid.Ref
end resourceid.Ref
stop ownership.OwnerState
}{
{"through a ReplicaSet", ownership.Unrestricted(), at("", "Pod", "alpha", "web-1-a"), at("apps", "Deployment", "alpha", "web"), 0},
{"an object with no controller", ownership.Unrestricted(), at("apps", "Deployment", "alpha", "web"), at("apps", "Deployment", "alpha", "web"), 0},
{"stops at a controller that's gone", ownership.Unrestricted(), stray, stray, ownership.OwnerGone},
{"stops at a withheld controller", access{denied: []resourceid.GroupKind{replicaSets}}, at("", "Pod", "alpha", "web-1-a"), at("", "Pod", "alpha", "web-1-a"), ownership.OwnerWithheld},
{"ends at a Node, outside the chain", ownership.Unrestricted(), at("example.io", "Gadget", "beta", "g1"), at("example.io", "Gadget", "beta", "g1"), 0},
} {
t.Run(tc.name, func(t *testing.T) {
got := view(t, w, tc.access).ControllerChain(tc.start)
stop := ownership.OwnerState(0)
if got.UnfollowedReference != nil {
stop = got.UnfollowedReference.State
}
if got.Chain[len(got.Chain)-1] != tc.end || (got.UnfollowedReference != nil) != (tc.stop != 0) || stop != tc.stop || got.Cycle || got.Truncated {
t.Fatalf("ControllerChain = %+v (stop %v); want to end at %v, stop %v", got, stop, tc.end, tc.stop)
}
})
}
}
func TestViewControllerChainTerminals(t *testing.T) {
b := ownershiptest.NewBuilder()
missingNode := metav1.OwnerReference{APIVersion: "v1", Kind: "Node", Name: "gone", UID: "uid-gone"}
yes := true
missingNode.Controller = &yes
mirror := b.Add("", "Pod", "kube-system", "etcd-gone", missingNode)
refTo := func(name string) metav1.OwnerReference {
return metav1.OwnerReference{APIVersion: "example.io/v1", Kind: "Widget", Name: name, UID: types.UID("uid-x-" + name), Controller: &yes}
}
loopA := b.Add("example.io", "Widget", "x", "a", refTo("b"))
loopB := b.Add("example.io", "Widget", "x", "b", refTo("a"))
tail := b.Add("example.io", "Widget", "x", "tail", refTo("b"))
const length = 48
for i := range length {
b.Add("example.io", "Widget", "chain", fmt.Sprint(i), metav1.OwnerReference{APIVersion: "example.io/v1", Kind: "Widget", Name: fmt.Sprint(i + 1), UID: types.UID(fmt.Sprintf("uid-chain-%d", i+1)), Controller: &yes})
}
v := view(t, b.World(), ownership.Unrestricted())
if got := v.ControllerChain(mirror.Ref); !slices.Equal(got.Chain, []resourceid.Ref{mirror.Ref}) || got.UnfollowedReference == nil || got.UnfollowedReference.State != ownership.OwnerGone {
t.Errorf("a mirror Pod of a deleted Node = %+v; want itself alone, stopped by the Node, which is gone", got)
}
for _, start := range []ownershiptest.Object{loopA, loopB, tail} {
if got := v.ControllerChain(start.Ref); !got.Cycle || !slices.Contains(got.Chain, loopA.Ref) || !slices.Contains(got.Chain, loopB.Ref) || got.UnfollowedReference != nil {
t.Errorf("from %v = %+v; want a cycle through a and b from any start", start.Ref, got)
}
}
if got := v.ControllerChain(at("example.io", "Widget", "chain", "0")); !got.Truncated || got.Chain[len(got.Chain)-1] != at("example.io", "Widget", "chain", "16") || len(got.Chain) != 17 {
t.Errorf("an over-long chain = %+v; want a walk bounded at 16 references", got)
}
if got := v.ControllerChain(tail.Ref); !slices.Equal(got.Chain, []resourceid.Ref{tail.Ref, loopB.Ref, loopA.Ref}) {
t.Errorf("chain into a loop = %v; want tail, b, a", got.Chain)
}
}
func TestViewControllerChainFollowsEachController(t *testing.T) {
v := view(t, ownershiptest.Fixture(), ownership.Unrestricted())
pod := at("", "Pod", "alpha", "web-1-a")
want := []resourceid.Ref{pod, at("apps", "ReplicaSet", "alpha", "web-1"), at("apps", "Deployment", "alpha", "web")}
if got := v.ControllerChain(pod); !slices.Equal(got.Chain, want) {
t.Fatalf("chain = %v, want %v", got.Chain, want)
}
stray := at("", "Pod", "beta", "stray")
if got := v.ControllerChain(stray); !slices.Equal(got.Chain, []resourceid.Ref{stray}) {
t.Fatalf("chain stopped at the first reference = %v", got.Chain)
}
}
func TestViewMembers(t *testing.T) {
w := ownershiptest.Fixture()
app := func(name string) resourceid.Ref { return at("argoproj.io", "Application", "argocd", name) }
refs := func(a ownership.MembersAnswer) []resourceid.Ref {
var out []resourceid.Ref
for _, m := range a.Members {
out = append(out, m.Ref)
}
return out
}
full := view(t, w, ownership.Unrestricted()).Members(app("web"))
want := []resourceid.Ref{
at("", "ConfigMap", "alpha", "cfg"), at("", "Node", "", "n1"), at("apps", "Deployment", "alpha", "web"),
at("example.io", "Gadget", "beta", "g1"), at("example.io", "Node", "alpha", "n1"), at("example.io", "Widget", "alpha", "w1"),
}
unserved := ownership.Gap{Kind: resourceid.GroupKind{Group: "example.io", Kind: "Unserved"}, Namespace: "alpha", Reason: readreason.KindUnknown}
if full.Membership != ownership.MembersRecorded || !slices.Equal(refs(full), want) || full.Missing != 1 || full.Withheld != 0 || full.Unknown != 1 || !slices.Equal(full.Gaps, []ownership.Gap{unserved}) {
t.Fatalf("members = %v %v missing %d withheld %d unknown %d gaps %+v", full.Membership, refs(full), full.Missing, full.Withheld, full.Unknown, full.Gaps)
}
alpha := view(t, w, access{namespaces: []string{"alpha", "argocd"}}).Members(app("web"))
if slices.Contains(refs(alpha), at("example.io", "Gadget", "beta", "g1")) || alpha.Withheld != 1 || len(alpha.Members) != len(want)-1 {
t.Fatalf("members a caller in alpha sees = %v withheld %d", refs(alpha), alpha.Withheld)
}
unsure := view(t, w, access{undecided: []resourceid.GroupKind{configMaps}}).Members(app("web"))
checkFailed := ownership.Gap{Kind: configMaps, Namespace: "alpha", Reason: readreason.UserPermissionCheckFailed}
if slices.Contains(refs(unsure), at("", "ConfigMap", "alpha", "cfg")) || unsure.Unknown != 2 || unsure.Withheld != 0 || !slices.Equal(unsure.Gaps, []ownership.Gap{checkFailed, unserved}) {
t.Fatalf("members when the caller's ConfigMap access couldn't be checked = %v unknown %d withheld %d gaps %+v", refs(unsure), unsure.Unknown, unsure.Withheld, unsure.Gaps)
}
for name, state := range map[string]ownership.MembershipState{"elsewhere": ownership.MembersRemote, "new": ownership.MembersUnreadable, "empty-list": ownership.MembersRecorded} {
if got := view(t, w, ownership.Unrestricted()).Members(app(name)); got.Membership != state || len(got.Members) != 0 || got.Missing+got.Withheld+got.Unknown != 0 {
t.Errorf("%s = %+v; want %v and no members", name, got, state)
}
}
}
// A reference carrying an owner's UID under other coordinates never resolves
// to the owner, since UIDs are unique: however the read of those coordinates
// goes, the object isn't a child and leaves the answer complete. A reference
// Radar can't map to a kind may still be the owner's, and stays a gap.
func TestViewChildrenIgnoreReferencesThatCantNameTheOwner(t *testing.T) {
b := ownershiptest.NewBuilder()
rs := b.Add("apps", "ReplicaSet", "alpha", "web")
b.Add("", "Pod", "alpha", "web-a", rs.OwnerRef(true))
odd := rs.OwnerRef(false)
odd.APIVersion, odd.Kind = "v1", "Node"
b.Add("", "Pod", "alpha", "odd", odd)
elsewhere := rs.OwnerRef(false)
elsewhere.APIVersion, elsewhere.Name = "apps/v9", "other"
b.Add("", "Pod", "alpha", "elsewhere", elsewhere)
w := b.World()
w.Collection[resourceid.GroupKind{Kind: "Node"}] = ownershiptest.Collection{State: ownershiptest.Loading}
if got := view(t, w, ownership.Unrestricted()).Children(rs.Ref); !got.Complete() || len(got.Children) != 1 {
t.Fatalf("children %q gaps %+v; a Node named web, or a reference to another name, can't be to the ReplicaSet", childRefs(got), got.Gaps)
}
unmapped := rs.OwnerRef(false)
unmapped.APIVersion = "apps/v9"
b.Add("", "Pod", "alpha", "unmapped", unmapped)
got := view(t, b.World(), ownership.Unrestricted()).Children(rs.Ref)
if want := (ownership.Gap{Kind: rs.Ref.GroupKind(), Reason: readreason.KindUnknown}); !slices.Contains(got.Gaps, want) {
t.Fatalf("gaps %+v; want %+v for a reference to web at a version discovery doesn't list", got.Gaps, want)
}
}
// An object naming its owner twice is one child.
func TestViewChildrenListAnObjectNamingItsOwnerTwiceOnce(t *testing.T) {
b := ownershiptest.NewBuilder()
rs := b.Add("apps", "ReplicaSet", "alpha", "web")
b.Add("", "Pod", "alpha", "dup", rs.OwnerRef(true), rs.OwnerRef(false))
v := view(t, b.World(), ownership.Unrestricted())
v.Owner(at("", "Pod", "alpha", "dup"))
if got := v.Children(rs.Ref); len(got.Children) != 1 {
t.Fatalf("children = %q; want dup once", childRefs(got))
}
}
// A manager may list an Event; ownership doesn't read Events, so the answer
// leaves it out rather than counting a member it can't say anything about.
func TestViewMembersLeaveOutEvents(t *testing.T) {
b := ownershiptest.NewBuilder()
cm := b.Add("", "ConfigMap", "alpha", "cm")
app := b.Manager(ownership.ArgoApplication, "argocd", "app", ownership.MembersRecorded, "alpha", at("", "Event", "alpha", "e"), cm.Ref)
for name, a := range map[string]ownership.Access{"everything": ownership.Unrestricted(), "no Events": access{denied: []resourceid.GroupKind{{Kind: "Event"}}}} {
got := view(t, b.World(), a).Members(app.Ref)
if len(got.Members) != 1 || got.Missing+got.Withheld+got.Unknown != 0 || len(got.Gaps) != 0 {
t.Fatalf("%s: members %d missing %d withheld %d unknown %d gaps %+v; want the ConfigMap alone", name, len(got.Members), got.Missing, got.Withheld, got.Unknown, got.Gaps)
}
}
}
func TestDecisionNames(t *testing.T) {
for d, want := range map[ownership.Decision]string{ownership.Denied: "denied", ownership.Allowed: "allowed", ownership.Undecided: "undecided", ownership.Decision(99): "denied"} {
if got := d.String(); got != want {
t.Errorf("Decision(%d) = %q, want %q", d, got, want)
}
}
}
// Owner and Children are one relation read from both ends: an object is its
// owner's child exactly when its reference to that owner resolves.
func TestViewOwnersAndChildrenAgree(t *testing.T) {
worlds := []ownershiptest.World{ownershiptest.Fixture()}
for seed := range uint64(30) {
worlds = append(worlds, ownershiptest.Generate(seed))
}
for i, w := range worlds {
v := view(t, w, ownership.Unrestricted())
for _, o := range w.Objects {
subject, resolutions := v.References(o.Ref)
if !subject.Present() {
continue
}
for _, r := range resolutions {
if r.State != ownership.OwnerFound {
continue
}
children := v.Children(r.Ref)
if !children.Complete() {
continue
}
if !slices.ContainsFunc(children.Children, func(c ownership.Observation) bool { return c.Ref == o.Ref }) {
t.Errorf("world %d: %v resolves its owner %v, which doesn't list it among %q", i, o.Ref, r.Ref, childRefs(children))
}
}
for _, c := range v.Children(o.Ref).Children {
_, theirs := v.References(c.Ref)
if !slices.ContainsFunc(theirs, func(r ownership.OwnerResolution) bool { return r.State == ownership.OwnerFound && r.Ref == o.Ref }) {
t.Errorf("world %d: %v lists %v, whose references don't resolve to it", i, o.Ref, c.Ref)
}
}
}
}
}
// A caller who may read less gets withheld or narrowed answers, never a
// different named one, and never a name it may not read.
func TestViewAnswersOnlyNarrowForACallerWhoMayReadLess(t *testing.T) {
worlds := []ownershiptest.World{ownershiptest.Fixture()}
for seed := range uint64(30) {
worlds = append(worlds, ownershiptest.Generate(seed))
}
callers := []access{
{namespaces: []string{"alpha"}},
{namespaces: []string{"beta", "gamma"}},
{namespaces: []string{}},
{denied: []resourceid.GroupKind{replicaSets}},
{denied: []resourceid.GroupKind{{Kind: "Node"}, {Group: "example.io", Kind: "Widget"}}},
{namespaces: []string{"alpha"}, undecided: []resourceid.GroupKind{configMaps}},
}
for i, w := range worlds {
full := view(t, w, ownership.Unrestricted())
for _, caller := range callers {
v := view(t, w, caller)
for _, o := range w.Objects {
checkNarrows(t, fmt.Sprintf("world %d, %+v, %v", i, caller, o.Ref), full, v, caller, o.Ref)
}
}
}
}
func checkNarrows(t *testing.T, where string, full, v *ownership.View, caller access, ref resourceid.Ref) {
t.Helper()
readable := func(r resourceid.Ref) bool { return caller.CanRead(r.GroupKind(), r.Namespace) == ownership.Allowed }
subject, got := v.References(ref)
if !readable(ref) {
if subject.Present() || got != nil {
t.Errorf("%s: answers for an object the caller may not read", where)
}
return
}
_, want := full.References(ref)
if len(got) != len(want) {
t.Fatalf("%s: %d references, want %d", where, len(got), len(want))
}
for i := range got {
switch {
case got[i].State == want[i].State && got[i].Ref == want[i].Ref && got[i].Gap == want[i].Gap:
case got[i].Ref == want[i].Ref && !readable(want[i].Ref) && (got[i].State == ownership.OwnerWithheld || got[i].Gap.Reason == readreason.UserPermissionCheckFailed):
default:
t.Errorf("%s: reference %d = %v %v %+v, full %v %v %+v", where, i, got[i].State, got[i].Ref, got[i].Gap, want[i].State, want[i].Ref, want[i].Gap)
}
}
children, all := v.Children(ref), full.Children(ref)
for _, c := range children.Children {
if !readable(c.Ref) || !slices.ContainsFunc(all.Children, func(f ownership.Observation) bool { return f.Ref == c.Ref }) {
t.Errorf("%s: child %v isn't one the full view has and the caller may read", where, c.Ref)
}
}
if len(children.Children) < len(all.Children) && !children.Narrowed {
t.Errorf("%s: %d of %d children without saying the answer is narrowed", where, len(children.Children), len(all.Children))
}
chain, fullChain := v.ControllerChain(ref), full.ControllerChain(ref)
if !slices.Equal(chain.Chain, fullChain.Chain) && (chain.UnfollowedReference == nil || chain.UnfollowedReference.State != ownership.OwnerWithheld && chain.UnfollowedReference.Gap.Reason != readreason.UserPermissionCheckFailed) {
t.Errorf("%s: chain %v, full %v, without a withheld stop", where, chain.Chain, fullChain.Chain)
}
if ownership.IsRecordingManager(ref.GroupKind()) {
members, allMembers := v.Members(ref), full.Members(ref)
for _, m := range members.Members {
if !readable(m.Ref) || !slices.ContainsFunc(allMembers.Members, func(f ownership.Observation) bool { return f.Ref == m.Ref }) {
t.Errorf("%s: member %v isn't one the full view has and the caller may read", where, m.Ref)
}
}
if hidden := len(allMembers.Members) - len(members.Members); hidden > members.Withheld+members.Unknown {
t.Errorf("%s: %d members dropped, %d withheld and %d unknown", where, hidden, members.Withheld, members.Unknown)
}
}
}
// managersWithGaps answers RecordingManagers with the session's answer plus gaps.
type managersWithGaps struct {
ownership.ReadSession
gaps []ownership.Gap
}
func (s managersWithGaps) RecordingManagers(ref resourceid.Ref) ownership.RecordingManagersRead {
r := s.ReadSession.RecordingManagers(ref)
r.Gaps = append(r.Gaps, s.gaps...)
return r
}
// A caller learns which manager kinds Radar couldn't search, and where among
// the namespaces it may read, but nothing of the namespaces it can't.
func TestRecordingManagersKeepGapsTheCallerMayRead(t *testing.T) {
s, err := ownershiptest.Scan(ownershiptest.Fixture()).Begin()
if err != nil {
t.Fatal(err)
}
everywhere := ownership.Gap{Kind: ownership.FluxHelmRelease, Reason: readreason.KindSyncPending}
alpha := ownership.Gap{Kind: ownership.ArgoApplication, Namespace: "alpha", Reason: readreason.KindSyncPending}
beta := ownership.Gap{Kind: ownership.ArgoApplication, Namespace: "beta", Reason: readreason.RadarForbidden}
v := ownership.NewView(managersWithGaps{s, []ownership.Gap{everywhere, alpha, beta}}, access{namespaces: []string{"alpha"}})
got := v.RecordingManagers(at("", "ConfigMap", "alpha", "cfg"))
if !slices.Contains(got.Gaps, everywhere) || !slices.Contains(got.Gaps, alpha) || slices.Contains(got.Gaps, beta) {
t.Fatalf("gaps = %v; want %v and %v, not %v", got.Gaps, everywhere, alpha, beta)
}
}