pkg/ownership/view.go
Rama main · réplica de lectura del repositorio original.
package ownership
import (
"cmp"
"slices"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/skyhook-io/radar/pkg/readreason"
"github.com/skyhook-io/radar/pkg/resourceid"
)
// View answers ownership questions for one caller from one session.
//
// It captures each object's metadata once, whether it read the object or
// found it searching an owner's dependents, and answers every question from
// that capture, so an object reads the same in every answer. Member lists
// aren't captured: Members reads a manager's status each time it is asked,
// claim checks capture its membership state once, and RecordingManagers searches
// for an object's managers once. Answers read the
// cache as it is when asked, so one can be a moment older than another, as
// any informer read can. It searches each owner's dependents at most once.
// It never reads an object the caller may not read, and says nothing of
// whether there is one. Its answers hold only if the session's
// ValidateCaches passes once they are built. They share the view's captures,
// so they must not be modified. A View is not safe for concurrent use.
type View struct {
session ReadSession
access Access
reads map[resourceid.Ref]accessRead
searches map[resourceid.Ref]DependentCandidatesRead
recording map[resourceid.Ref]RecordingManagersAnswer
claimMembership map[resourceid.Ref]MembersRead
unreadable []resourceid.Ref
}
type accessRead struct {
read ObjectRead
may Decision
}
// NewView answers for the caller access describes, from s.
func NewView(s ReadSession, access Access) *View {
return &View{
session: s,
access: access,
reads: map[resourceid.Ref]accessRead{},
searches: map[resourceid.Ref]DependentCandidatesRead{},
recording: map[resourceid.Ref]RecordingManagersAnswer{},
claimMembership: map[resourceid.Ref]MembersRead{},
}
}
// read reads ref for the caller, once.
func (v *View) read(ref resourceid.Ref) (ObjectRead, Decision) {
if r, ok := v.reads[ref]; ok {
return r.read, r.may
}
r := accessRead{may: v.access.CanRead(ref.GroupKind(), ref.Namespace)}
if r.may == Allowed {
r.read = v.session.Object(ref)
}
v.reads[ref] = r
return r.read, r.may
}
// Seed reads o as its object when the session can't, as when the caller
// fetched it from the API and Radar doesn't collect its kind. A read that
// holds the object stands, as does an excluded one or one the caller may not
// make. Seed an object before asking about it, so every answer reads it the
// same way. The view keeps o, which must not be modified afterwards.
func (v *View) Seed(o Observation) {
if read, may := v.read(o.Ref); may == Allowed && (read.State == Unavailable || read.State == Absent) {
v.reads[o.Ref] = accessRead{read: ObjectRead{State: Present, Object: o}, may: Allowed}
}
}
// Subject is the object an answer is about, as the caller read it.
type Subject struct {
Read ObjectRead
// Access is whether the caller may read the object. When it may not,
// Read is unset and the answer says nothing more.
Access Decision
}
// Present reports whether the caller read the object, so the answer has an
// object to be about.
func (s Subject) Present() bool {
return s.Access == Allowed && s.Read.State == Present
}
func (v *View) subject(ref resourceid.Ref) Subject {
read, may := v.read(ref)
return Subject{Read: read, Access: may}
}
func (v *View) resolve(dependent Observation, ref metav1.OwnerReference) OwnerResolution {
return resolveOwner(v.session.Catalog(), dependent, ref, v.read)
}
// References resolves each of ref's owner references for the caller, in the
// object's order.
func (v *View) References(ref resourceid.Ref) (Subject, []OwnerResolution) {
subject := v.subject(ref)
if !subject.Present() {
return subject, nil
}
refs := subject.Read.Object.OwnerReferences
out := make([]OwnerResolution, len(refs))
for i, r := range refs {
out[i] = v.resolve(subject.Read.Object, r)
}
return subject, out
}
// OwnerAnswer is an object's owner reference: its controller reference, or
// its only reference when it has no controller.
type OwnerAnswer struct {
Subject Subject
// Reference is the owner reference chosen, one of the subject's own. It is
// nil when there is none: the subject wasn't read, it has no owner
// references, or several without a controller (Several). Resolution means
// something only when Reference is set.
Reference *metav1.OwnerReference
Resolution OwnerResolution
Several bool
}
// Owner resolves ref's controller reference, or its only reference. The
// reference answers in whatever state it resolves to: a withheld or replaced
// controller never gives way to a weaker owner.
func (v *View) Owner(ref resourceid.Ref) OwnerAnswer {
subject, resolutions := v.References(ref)
a := OwnerAnswer{Subject: subject}
if !subject.Present() {
return a
}
refs := subject.Read.Object.OwnerReferences
i := slices.IndexFunc(refs, isController)
if i < 0 && len(refs) == 1 {
i = 0
}
if i < 0 {
a.Several = len(refs) > 1
return a
}
a.Reference, a.Resolution = &refs[i], resolutions[i]
return a
}
func isController(ref metav1.OwnerReference) bool {
return ref.Controller != nil && *ref.Controller
}
// ChildrenAnswer is what an object owns, as one caller may see it.
type ChildrenAnswer struct {
Subject Subject
// Children are the objects whose owner reference names the subject's
// current incarnation, ordered by group, kind, namespace and name.
Children []Observation
// Gaps are the parts of the search the source couldn't read (see
// DependentCandidatesRead.Gaps), objects the search found that the view couldn't
// read, and references to the subject's UID that may resolve to it but
// couldn't be resolved, such as one at a version discovery doesn't list.
Gaps []Gap
// Narrowed: the caller's access is restricted, so Children are the ones
// it may read, and whether there are others isn't said.
Narrowed bool
}
// Complete reports whether Children are every child the caller may read. An
// answer about an object the caller didn't read lists none and is never
// complete.
func (a ChildrenAnswer) Complete() bool {
return a.Subject.Present() && len(a.Gaps) == 0
}
// Children finds the objects with an owner reference that resolves to ref's
// current incarnation, as Owner resolves it: ref's group, kind and name, a
// served version, and ref's UID. A namespaced owner's children are in its
// namespace, where Kubernetes resolves their references; a cluster-scoped
// owner's can be anywhere. A reference with the UID that names other
// coordinates is not one Kubernetes resolves to ref.
func (v *View) Children(ref resourceid.Ref) ChildrenAnswer {
subject := v.subject(ref)
a := ChildrenAnswer{Subject: subject}
if !subject.Present() {
return a
}
owner := subject.Read.Object
namespaces, all := []string{ref.Namespace}, false
if ref.Namespace == "" {
namespaces, all = v.access.Namespaces()
}
scope := SearchScope{AllNamespaces: all, Namespaces: namespaces, IncludeClusterScoped: ref.Namespace == ""}
found, searched := v.searches[ref]
if !searched {
found = v.session.DependentCandidates(owner.UID, scope)
v.searches[ref] = found
}
candidates, unread, undecided := v.candidates(found)
// Saying a check failed would say a child may be there.
a.Narrowed = v.access != Unrestricted() || undecided
// unresolved are the gaps of references to the owner's UID that Radar
// can't resolve: the collector retries them, so their objects may be
// children.
var unresolved []Gap
for _, c := range candidates {
names := false
var unknown []Gap
for _, r := range c.OwnerReferences {
if r.UID != owner.UID {
continue
}
switch res := v.resolve(c, r); {
case res.State == OwnerFound && res.Ref == ref:
names = true
case res.State == OwnerUnknown && mayResolveTo(res, r.Name, ref):
unknown = append(unknown, res.Gap)
}
}
if !names {
unresolved = append(unresolved, unknown...)
continue
}
a.Children = append(a.Children, c)
}
a.Gaps = found.Gaps
for i, g := range found.Gaps {
if !v.coversReadable(g, scope) {
a.Gaps = slices.Clone(found.Gaps[:i])
for _, later := range found.Gaps[i+1:] {
if v.coversReadable(later, scope) {
a.Gaps = append(a.Gaps, later)
}
}
break
}
}
if len(unread)+len(unresolved) > 0 {
a.Gaps = slices.Clone(a.Gaps)
a.Gaps = append(a.Gaps, unread...)
a.Gaps = append(a.Gaps, unresolved...)
}
for i := 1; i < len(a.Gaps); i++ {
if compareGaps(a.Gaps[i-1], a.Gaps[i]) >= 0 {
a.Gaps = slices.Clone(a.Gaps)
slices.SortFunc(a.Gaps, compareGaps)
a.Gaps = slices.Compact(a.Gaps)
break
}
}
return a
}
// mayResolveTo reports whether a reference naming ref's UID, whose resolution
// is unknown, may still resolve to ref: it names ref's name, and a kind Radar
// resolved to ref's or couldn't resolve with ref's group and kind. UIDs are unique,
// so a reference to other coordinates never resolves to the object holding
// its UID, however the read of those coordinates goes.
func mayResolveTo(res OwnerResolution, name string, ref resourceid.Ref) bool {
switch {
case name != ref.Name:
return false
case res.Ref != (resourceid.Ref{}):
return res.Ref.GroupKind() == ref.GroupKind()
}
return res.Gap.Kind.Group == ref.Group && strings.EqualFold(res.Gap.Kind.Kind, ref.Kind)
}
// candidates are the objects the search found that the caller may read, each
// as the view captured it: an object it already read answers as it was read,
// whatever the search found since, and one the search found is captured as
// found. unread are the gaps of found objects the view read before it could
// tell whether they exist: the search says they name the owner, so leaving
// them out says nothing of whether they are children. undecided reports a
// candidate whose access check failed.
func (v *View) candidates(found DependentCandidatesRead) (out []Observation, unread []Gap, undecided bool) {
for _, c := range found.Candidates {
r, read := v.reads[c.Ref]
if !read {
r = accessRead{may: v.access.CanRead(c.Ref.GroupKind(), c.Ref.Namespace)}
if r.may == Allowed {
r.read = ObjectRead{State: Present, Object: c}
}
v.reads[c.Ref] = r
}
switch {
case r.may == Undecided:
undecided = true
case r.may != Allowed:
case r.read.State == Present:
out = append(out, r.read.Object)
case r.read.State == Unavailable:
unread = append(unread, r.read.Gap)
}
}
return out, unread, undecided
}
// coversReadable reports whether gap covers something the caller may read
// within scope, so leaving it unread may have hidden a child. A gap on a
// namespaced kind without a namespace covers every namespace of the scope,
// and counts if the caller may read the kind in any of them.
func (v *View) coversReadable(g Gap, scope SearchScope) bool {
if g.Kind.Kind == "" {
return true
}
if g.Namespace != "" {
return v.access.CanRead(g.Kind, g.Namespace) != Denied
}
k, known := v.session.Catalog().Kind(g.Kind)
if !known || !k.Namespaced {
return v.access.CanRead(g.Kind, "") != Denied
}
if scope.AllNamespaces {
return v.access.CanRead(g.Kind, "") != Denied
}
return slices.ContainsFunc(scope.Namespaces, func(ns string) bool { return v.access.CanRead(g.Kind, ns) != Denied })
}
// ControllerChainAnswer is the chain of controller references from an object
// up to the first one that can't be followed.
type ControllerChainAnswer struct {
Subject Subject
// Chain is every object the walk reached, from the subject on.
Chain []resourceid.Ref
// UnfollowedReference is the controller reference the walk couldn't
// follow. It is nil when the last object in Chain has no controller
// reference, and when the walk ends in a Cycle or is Truncated. A Node
// controller ends the walk outside Chain, and is UnfollowedReference only
// when it isn't found.
UnfollowedReference *OwnerResolution
// Cycle: the references loop back into Chain.
Cycle bool
// Truncated: the chain is longer than maxControllerDepth references,
// which only corrupt references produce.
Truncated bool
}
// maxControllerDepth bounds the walk; real chains are a few hops deep.
const maxControllerDepth = 16
// ControllerChain follows ref's controller references to the first it can't
// follow, and says why it stopped.
func (v *View) ControllerChain(ref resourceid.Ref) ControllerChainAnswer {
subject := v.subject(ref)
a := ControllerChainAnswer{Subject: subject}
if !subject.Present() {
return a
}
a.Chain = []resourceid.Ref{ref}
current := subject.Read.Object
for {
i := slices.IndexFunc(current.OwnerReferences, isController)
if i < 0 {
return a
}
r := v.resolve(current, current.OwnerReferences[i])
if r.Ref.Group == "" && r.Ref.Kind == "Node" {
if r.State != OwnerFound {
a.UnfollowedReference = &r
}
return a
}
if r.State != OwnerFound {
a.UnfollowedReference = &r
return a
}
if slices.Contains(a.Chain, r.Ref) {
a.Cycle = true
return a
}
if len(a.Chain) > maxControllerDepth {
a.Truncated = true
return a
}
a.Chain = append(a.Chain, r.Ref)
current = r.Owner
}
}
func compareRefs(a, b resourceid.Ref) int {
return cmp.Or(cmp.Compare(a.Group, b.Group), cmp.Compare(a.Kind, b.Kind), cmp.Compare(a.Namespace, b.Namespace), cmp.Compare(a.Name, b.Name))
}
// MembersAnswer is what a GitOps manager's status records of this cluster's
// objects, as one caller may see it.
type MembersAnswer struct {
Subject Subject
// Membership is what the manager's status records; Members and the counts
// are set only when it is MembersRecorded. A remote manager's destination
// isn't confirmed to be this cluster, so none of its list is shown here.
Membership MembershipState
// Members are the recorded objects Radar holds that the caller may read,
// sorted.
Members []Observation
// Missing counts the recorded objects Radar has loaded the stores of and
// doesn't hold. Withheld counts those the caller may not read, and
// Unknown those Radar can't say whether it holds (Gaps say why). The
// manager's status, which the caller can read, names them all; Events,
// which ownership doesn't read, are left out.
Missing, Withheld, Unknown int
Gaps []Gap
}
// Members reads the member list in the status of manager, which must be of a
// RecordingManagers kind, and reads each member.
func (v *View) Members(manager resourceid.Ref) MembersAnswer {
subject := v.subject(manager)
a := MembersAnswer{Subject: subject}
if !subject.Present() || !IsRecordingManager(manager.GroupKind()) {
return a
}
inventory := v.session.Members(manager)
switch {
case inventory.Manager.State == Unavailable:
// As when the caller fetched a manager Radar doesn't collect.
a.Gaps = append(a.Gaps, inventory.Manager.Gap)
return a
case inventory.Manager.State != Present || inventory.Manager.Object.UID != subject.Read.Object.UID:
// The cache doesn't hold the incarnation the caller fetched yet.
a.Gaps = append(a.Gaps, Gap{Kind: manager.GroupKind(), Namespace: manager.Namespace, Reason: readreason.KindSyncPending})
return a
}
a.Membership = inventory.Membership
if a.Membership != MembersRecorded {
return a
}
for _, m := range inventory.Members {
if IsEvent(m.GroupKind()) {
continue
}
read, may := v.read(m)
switch {
case may == Denied:
a.Withheld++
case may == Undecided:
a.Unknown++
a.Gaps = append(a.Gaps, Gap{Kind: m.GroupKind(), Namespace: m.Namespace, Reason: readreason.UserPermissionCheckFailed})
case read.State == Present:
a.Members = append(a.Members, read.Object)
case read.State == Absent:
a.Missing++
case read.State == Unavailable:
a.Unknown++
a.Gaps = append(a.Gaps, read.Gap)
}
}
slices.SortFunc(a.Gaps, compareGaps)
a.Gaps = slices.Compact(a.Gaps)
return a
}
func compareGaps(a, b Gap) int {
return cmp.Or(cmp.Compare(a.Kind.Group, b.Kind.Group), cmp.Compare(a.Kind.Kind, b.Kind.Kind), cmp.Compare(a.Namespace, b.Namespace), cmp.Compare(a.Reason, b.Reason))
}
// RecordingManagersAnswer is what the local GitOps managers say of one object, as one
// caller may see them.
type RecordingManagersAnswer struct {
// Managers are the managers the caller may read whose member lists name
// the object, sorted.
Managers []Observation
// Unreadable are the managers the caller may read whose member lists
// can't be read, any of which may name the object, sorted.
Unreadable []resourceid.Ref
// Remote are the managers the caller may read whose lists name an
// object at the same coordinates in a cluster Radar can't confirm is this
// one, sorted.
Remote []resourceid.Ref
// Gaps are the search's gaps the caller may read; see RecordingManagersRead.Gaps.
Gaps []Gap
// Undecided: Radar couldn't check whether the caller may read some of the
// managers or gaps the search found, so the answer may lack some it may.
Undecided bool
// searchGaps are all the search's gaps, for Searched.
searchGaps []Gap
}
// Searched reports whether the search read the member list of manager, a
// manager the caller may read and Radar holds: no gap the search reported,
// whether or not the caller may read it, leaves manager's kind unread where
// manager is. A gap across all namespaces that stands for the namespaces
// Radar doesn't collect leaves manager's own read.
func (a RecordingManagersAnswer) Searched(manager resourceid.Ref) bool {
return !slices.ContainsFunc(a.searchGaps, func(g Gap) bool { return covers(g, manager) })
}
// covers reports whether a search's gap left ref's kind and place unread, ref
// being an object Radar holds.
func covers(g Gap, ref resourceid.Ref) bool {
switch {
case g.Kind != ref.GroupKind():
return false
case g.Namespace == ref.Namespace:
return true
case g.Namespace != "":
return false
}
return g.Reason != readreason.RadarNamespaceNotCollected
}
// RecordingManagers finds the local managers whose member lists name ref, among those
// the caller may read; a manager the caller may not read takes no part. A
// manager the view already read answers as it was read, and one it hadn't is
// captured as found.
func (v *View) RecordingManagers(ref resourceid.Ref) RecordingManagersAnswer {
if a, ok := v.recording[ref]; ok {
return a
}
a := v.recordingManagers(ref)
v.recording[ref] = a
return a
}
func (v *View) recordingManagers(ref resourceid.Ref) RecordingManagersAnswer {
found := v.session.RecordingManagers(ref)
a := RecordingManagersAnswer{searchGaps: found.Gaps}
may := func(gk resourceid.GroupKind, namespace string) bool {
d := v.access.CanRead(gk, namespace)
a.Undecided = a.Undecided || d == Undecided
return d == Allowed
}
for _, g := range found.Gaps {
// A gap the caller may not read would say what Radar collects where
// the caller can't look.
if may(g.Kind, g.Namespace) {
a.Gaps = append(a.Gaps, g)
}
}
for _, m := range found.Managers {
r, read := v.reads[m.Ref]
if !read {
r = accessRead{may: v.access.CanRead(m.Ref.GroupKind(), m.Ref.Namespace)}
if r.may == Allowed {
r.read = ObjectRead{State: Present, Object: m}
}
v.reads[m.Ref] = r
}
a.Undecided = a.Undecided || r.may == Undecided
if r.may != Allowed {
continue
}
// The view read this manager before the search found it: an earlier
// read that couldn't see it, or saw another incarnation, can't vouch
// for the list the search read, and isn't silently dropped either.
switch {
case r.read.State == Unavailable:
a.Gaps = append(a.Gaps, r.read.Gap)
case r.read.State != Present || r.read.Object.UID != m.UID:
a.Gaps = append(a.Gaps, Gap{Kind: m.Ref.GroupKind(), Namespace: m.Ref.Namespace, Reason: readreason.KindSyncPending})
default:
a.Managers = append(a.Managers, r.read.Object)
}
}
// Consecutive rows commonly see the same unreadable inventories. Share
// their filtered references without freezing index reads or permissions.
a.Unreadable = v.unreadable
shared, n := true, 0
for _, m := range found.Unreadable {
if may(m.GroupKind(), m.Namespace) {
if shared && (n == len(a.Unreadable) || a.Unreadable[n] != m) {
a.Unreadable = slices.Clone(a.Unreadable[:n])
shared = false
}
if !shared {
a.Unreadable = append(a.Unreadable, m)
}
n++
}
}
a.Unreadable = a.Unreadable[:n:n]
v.unreadable = a.Unreadable
for _, m := range found.Remote {
if may(m.GroupKind(), m.Namespace) {
a.Remote = append(a.Remote, m)
}
}
return a
}