pkg/ownership/view_capture_test.go
Rama main · réplica de lectura del repositorio original.
package ownership_test
import (
"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"
)
// countingSession counts the reads a view makes, and can move every read
// after the first search to another world, as the cluster would change
// between two reads of one request.
type countingSession struct {
now, later ownership.ReadSession
switchAt string
objects map[resourceid.Ref]int
searches map[types.UID]int
validate error
}
func newCountingSession(t *testing.T, w ownershiptest.World) *countingSession {
t.Helper()
s, err := ownershiptest.Scan(w).Begin()
if err != nil {
t.Fatal(err)
}
return &countingSession{now: s, objects: map[resourceid.Ref]int{}, searches: map[types.UID]int{}}
}
func (c *countingSession) after(method string) {
if c.later != nil && c.switchAt == method {
c.now, c.later = c.later, nil
}
}
func (c *countingSession) Catalog() *ownership.Catalog { return c.now.Catalog() }
func (c *countingSession) Object(ref resourceid.Ref) ownership.ObjectRead {
c.objects[ref]++
defer c.after("Object")
return c.now.Object(ref)
}
func (c *countingSession) DependentCandidates(uid types.UID, d ownership.SearchScope) ownership.DependentCandidatesRead {
c.searches[uid]++
defer c.after("DependentCandidates")
return c.now.DependentCandidates(uid, d)
}
func (c *countingSession) Members(ref resourceid.Ref) ownership.MembersRead {
return c.now.Members(ref)
}
func (c *countingSession) Membership(ref resourceid.Ref) ownership.MembersRead {
return c.now.Membership(ref)
}
func (c *countingSession) RecordingManagers(ref resourceid.Ref) ownership.RecordingManagersRead {
return c.now.RecordingManagers(ref)
}
func (c *countingSession) ValidateCaches() error {
if c.validate != nil {
return c.validate
}
return c.now.ValidateCaches()
}
func TestViewReadsEachObjectAndSearchesEachOwnerOnce(t *testing.T) {
s := newCountingSession(t, ownershiptest.Fixture())
v := ownership.NewView(s, ownership.Unrestricted())
rs := at("apps", "ReplicaSet", "alpha", "web-1")
for range 3 {
v.Owner(at("", "Pod", "alpha", "web-1-a"))
v.Children(rs)
v.ControllerChain(at("", "Pod", "alpha", "web-1-b"))
}
for ref, n := range s.objects {
if n != 1 {
t.Errorf("%v read %d times", ref, n)
}
}
for uid, n := range s.searches {
if n != 1 {
t.Errorf("dependents of %s searched %d times", uid, n)
}
}
}
// An answer that found nothing is still an answer: an owner without
// children isn't searched again.
func TestViewReadsOnceWhenThereIsNothingToFind(t *testing.T) {
b := ownershiptest.NewBuilder()
rs := b.Add("apps", "ReplicaSet", "alpha", "empty")
release := b.Add("helm.toolkit.fluxcd.io", "HelmRelease", "alpha", "legacy")
s := newCountingSession(t, b.World())
v := ownership.NewView(s, ownership.Unrestricted())
for range 3 {
if got := v.Children(rs.Ref); len(got.Children) != 0 {
t.Fatalf("children of an empty ReplicaSet = %v", got.Children)
}
}
if got := v.Members(release.Ref); got.Membership != ownership.MembersUnreadable {
t.Fatalf("a HelmRelease without a recorded list is %v", got.Membership)
}
if n := s.searches[rs.UID]; n != 1 {
t.Errorf("an owner without children searched %d times", n)
}
}
// An object the search found is captured as found: the Owner answer asked
// after it reflects that capture, though the object changed since.
func TestViewAgreesWithItselfWhileTheClusterChanges(t *testing.T) {
before := ownershiptest.Fixture()
after := ownershiptest.Fixture()
pod, rs := at("", "Pod", "alpha", "web-1-a"), at("apps", "ReplicaSet", "alpha", "web-1")
for i := range after.Objects {
if after.Objects[i].Ref == pod {
// Orphaned: the same Pod, no longer naming its ReplicaSet.
after.Objects[i].OwnerReferences = nil
}
}
owned := func(v *ownership.View) bool {
o := v.Owner(pod)
return o.Resolution.State == ownership.OwnerFound && o.Resolution.Ref == rs
}
listed := func(v *ownership.View) bool {
return slices.ContainsFunc(v.Children(rs).Children, func(c ownership.Observation) bool { return c.Ref == pod })
}
// Children first, then the Pod changes, then Owner.
s := newCountingSession(t, before)
later, _ := ownershiptest.Scan(after).Begin()
s.later, s.switchAt = later, "DependentCandidates"
v := ownership.NewView(s, ownership.Unrestricted())
if !listed(v) || !owned(v) {
t.Errorf("after the search found the Pod: listed=%v owned=%v; a view must agree with itself", listed(v), owned(v))
}
}
// An object the search finds again after the view read it answers as read:
// here the Pod's reference stops being its controller in between.
func TestViewAnswersFromItsCaptureWhenTheSearchFindsANewerCopy(t *testing.T) {
before := ownershiptest.Fixture()
after := ownershiptest.Fixture()
pod, rs := at("", "Pod", "alpha", "web-1-a"), at("apps", "ReplicaSet", "alpha", "web-1")
for i := range after.Objects {
if after.Objects[i].Ref == pod {
r := after.Objects[i].OwnerReferences[0]
r.Controller = nil
after.Objects[i].OwnerReferences = []metav1.OwnerReference{r}
after.Objects[i].ResourceVersion = "changed"
}
}
s := newCountingSession(t, before)
later, _ := ownershiptest.Scan(after).Begin()
s.later, s.switchAt = later, "Object"
v := ownership.NewView(s, ownership.Unrestricted())
if o := v.Owner(pod); o.Resolution.State != ownership.OwnerFound || o.Reference == nil || !isControllerRef(*o.Reference) {
t.Fatalf("scenario: the Pod's first read must name its ReplicaSet as controller")
}
i := slices.IndexFunc(v.Children(rs).Children, func(c ownership.Observation) bool { return c.Ref == pod })
if i < 0 || !isControllerRef(v.Children(rs).Children[i].OwnerReferences[0]) {
t.Fatalf("Children of the ReplicaSet must list the Pod as the controlled child the view read")
}
}
func isControllerRef(r metav1.OwnerReference) bool { return r.Controller != nil && *r.Controller }
// A Pod the view read while Pods loaded is unknown to it. Once they have
// loaded, the ReplicaSet's search finds the Pod naming it; leaving the Pod out
// must not leave the children complete, since the view can't say whether it
// is one.
func TestViewChildrenSayWhatTheyReadBeforeItCouldBeRead(t *testing.T) {
b := ownershiptest.NewBuilder()
rs := b.Add("apps", "ReplicaSet", "alpha", "web")
pod := b.Add("", "Pod", "alpha", "web-a", rs.OwnerRef(true))
loading, loaded := b.World(), b.World()
loading.Collection[pods] = ownershiptest.Collection{State: ownershiptest.Loading}
s := newCountingSession(t, loading)
later, err := ownershiptest.Scan(loaded).Begin()
if err != nil {
t.Fatal(err)
}
s.later, s.switchAt = later, "Object"
v := ownership.NewView(s, ownership.Unrestricted())
if got := v.Owner(pod.Ref); got.Subject.Read.State != ownership.Unavailable {
t.Fatalf("scenario: the Pod must read unavailable while Pods load, got %v", got.Subject.Read.State)
}
got := v.Children(rs.Ref)
want := []ownership.Gap{{Kind: pods, Namespace: "alpha", Reason: readreason.KindSyncPending}}
if len(got.Children) != 0 || !slices.Equal(got.Gaps, want) || got.Complete() {
t.Fatalf("Children after the Pods loaded = %q gaps %+v complete=%v; want none, and the gap of the Pod read before", childRefs(got), got.Gaps, got.Complete())
}
}
// A gap on a namespaced kind across every namespace counts when the caller
// may read that kind in any of them, or its access couldn't be checked. When
// it may read the kind nowhere, naming the gap would say how Radar's
// collection of a kind the caller can't read is going; the gap is left out,
// and the answer is narrowed anyway.
func TestViewAuthorizesAnAllNamespacesGapByKind(t *testing.T) {
w := ownershiptest.Fixture()
w.Collection[pods] = ownershiptest.Collection{State: ownershiptest.Loading}
node := at("", "Node", "", "n1")
podGap := func(g ownership.Gap) bool { return g.Kind == pods && g.Reason == readreason.KindSyncPending }
readsPods := view(t, w, access{denied: []resourceid.GroupKind{{Kind: "Secret"}}}).Children(node)
if readsPods.Complete() || !slices.ContainsFunc(readsPods.Gaps, podGap) {
t.Errorf("a caller who may read Pods everywhere: gaps %+v complete=%v; want the Pod gap", readsPods.Gaps, readsPods.Complete())
}
podsInAlpha := view(t, w, podsInAlphaOnly{}).Children(node)
if podsInAlpha.Complete() || !slices.ContainsFunc(podsInAlpha.Gaps, podGap) {
t.Errorf("a caller who may read Pods in one namespace: gaps %+v complete=%v; want the Pod gap", podsInAlpha.Gaps, podsInAlpha.Complete())
}
unsure := view(t, w, access{undecided: []resourceid.GroupKind{pods}}).Children(node)
if !slices.ContainsFunc(unsure.Gaps, podGap) || !unsure.Narrowed {
t.Errorf("a caller whose Pod access couldn't be checked: gaps %+v narrowed=%v; want the Pod gap, narrowed", unsure.Gaps, unsure.Narrowed)
}
noPods := view(t, w, access{denied: []resourceid.GroupKind{pods}}).Children(node)
if slices.ContainsFunc(noPods.Gaps, func(g ownership.Gap) bool { return g.Kind == pods }) || !noPods.Narrowed {
t.Errorf("a caller who may not read Pods: gaps %+v narrowed=%v; want no Pod gap, narrowed", noPods.Gaps, noPods.Narrowed)
}
}
// podsInAlphaOnly may read every namespace, Pods only in alpha.
type podsInAlphaOnly struct{}
func (podsInAlphaOnly) CanRead(gk resourceid.GroupKind, namespace string) ownership.Decision {
if gk == pods && namespace != "" && namespace != "alpha" {
return ownership.Denied
}
return ownership.Allowed
}
func (podsInAlphaOnly) Namespaces() ([]string, bool) { return nil, true }