Ctrl K
ComunidadGuías…

pkg/ownership/manager.go

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

Archivo original ↗Preguntar a KiBot
package ownership

import (
	"slices"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

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

// ManagerState sums up what the evidence says manages an object. It never
// names a winner the evidence doesn't support: a disagreement stays one.
//
// "Claims" in the states are the claims that count: a copied claim names
// another object's manager and never counts, and a native Helm release's
// counts only when no list names the object and no GitOps claim exists, since
// Flux's helm-controller and adopted installs carry its annotations too.
// Every claim stays in the evidence either way.
type ManagerState string

const (
	// ManagerShared: two or more local managers' status lists name it.
	ManagerShared ManagerState = "shared"
	// ManagerDisagree: one manager's list names it and its metadata names
	// another.
	ManagerDisagree ManagerState = "disagree"
	// ManagerAgree: one manager's list names it and every claim its metadata
	// makes names that manager.
	ManagerAgree ManagerState = "agree"
	// ManagerListed: one manager's list names it; no claim counts.
	ManagerListed ManagerState = "listed"
	// ManagerClaimed: only its metadata names a manager, whose list couldn't
	// confirm it (see each claim's Check).
	ManagerClaimed ManagerState = "claimed"
	// ManagerUnknown: no evidence names a manager, and Radar couldn't look
	// everywhere it would be.
	ManagerUnknown ManagerState = "unknown"
	// ManagerNone: no evidence names a manager in what Radar collects and
	// the caller may read. It is not a statement that nothing manages it.
	ManagerNone ManagerState = "none"
)

// ClaimCheck is what checking a claim against its manager found.
type ClaimCheck string

const (
	// ClaimRecorded: the claimed manager's list names the object.
	ClaimRecorded ClaimCheck = "recorded"
	// ClaimOmitted: the claimed manager's list was read and doesn't name it,
	// as while the list lags an apply or for a Helm hook.
	ClaimOmitted ClaimCheck = "omitted"
	// ClaimUnreadable: the claimed manager's list can't be read.
	ClaimUnreadable ClaimCheck = "unreadable"
	// ClaimRemote: the claimed manager applies to a cluster Radar can't
	// confirm is this one.
	ClaimRemote ClaimCheck = "remote"
	// ClaimMissing: no such manager exists in this cluster.
	ClaimMissing ClaimCheck = "missing"
	// ClaimNotRecording: the claimed manager keeps no list, as a native Helm
	// release.
	ClaimNotRecording ClaimCheck = "not-recording"
	// ClaimUnresolved: the claim names no namespace and no manager whose list
	// names the object has that name.
	ClaimUnresolved ClaimCheck = "unresolved"
	// ClaimWithheld: the caller may not read the claimed manager, so nothing
	// is said of it.
	ClaimWithheld ClaimCheck = "withheld"
	// ClaimUnchecked: Radar couldn't establish what the manager's list says:
	// it couldn't read or decide access to the manager, the manager was
	// replaced since the search, or the search didn't cover it. Unlike
	// ClaimUnreadable, the list itself may be fine.
	ClaimUnchecked ClaimCheck = "unchecked"
)

// CheckedClaim is a claim an object's metadata makes, and what checking it
// found.
type CheckedClaim struct {
	gitops.Claim
	Check ClaimCheck
	// Weak: the claim doesn't stop the walk up the controller chain: it was
	// copied from another object, or it is metadata an object a controller
	// owns commonly inherits, an Argo CD instance label from its template or
	// a Helm release's annotations from its Deployment. A weak claim that
	// isn't copied can still answer when nothing on the chain settles it.
	Weak bool
}

// ManagerEvidence is what names the managers of one object.
type ManagerEvidence struct {
	Object resourceid.Ref
	// Recorders are the local managers the caller may read whose lists name
	// the object, sorted.
	Recorders []resourceid.Ref
	Claims    []CheckedClaim
	// Unreadable and Remote count the managers the caller may read that may
	// name the object without Radar being able to tell; Gaps are the kinds
	// and places the search couldn't read.
	Unreadable, Remote int
	Gaps               []Gap
	// Undecided: Radar couldn't check whether the caller may read some of
	// what the search found.
	Undecided bool
}

func (e ManagerEvidence) complete() bool {
	return e.Unreadable == 0 && e.Remote == 0 && len(e.Gaps) == 0 && !e.Undecided
}

// strong reports whether e settles which managers the object has: a list
// names it, or a claim that isn't weak does.
func (e ManagerEvidence) strong() bool {
	return len(e.Recorders) > 0 || slices.ContainsFunc(e.Claims, func(c CheckedClaim) bool { return !c.Weak })
}

// ChainEnd says why a walk up the controller chain that found no evidence
// settling the question stopped short. It is empty when evidence settled it
// first, whatever lies above, or when the walk reached the chain's top.
type ChainEnd string

const (
	ChainEndUnfollowed ChainEnd = "unfollowed"
	ChainEndCycle      ChainEnd = "cycle"
	ChainEndTruncated  ChainEnd = "truncated"
)

// ManagerAnswer is what manages one object, as one caller may see it.
type ManagerAnswer struct {
	Subject Subject
	State   ManagerState
	// Lead is the manager to show first, never a verdict: the recorder for
	// agree and listed; for shared, the one recorder a claim names, if exactly
	// one; for claimed, the manager every claim names, if they agree; none
	// otherwise. Its namespace is empty when it comes from a claim that
	// names none, and it may be a manager Radar couldn't read.
	Lead *resourceid.Ref
	// Direct is the evidence on the object itself.
	Direct ManagerEvidence
	// Via is the evidence of the nearest controller ancestor that settles
	// the question, when the object's own doesn't, or, when no object does
	// and the object makes no claim that isn't copied, of the nearest
	// ancestor that does; State and Lead are then derived from it.
	Via *ManagerEvidence
	// Between is the evidence of other objects the walk passed, in chain
	// order, that make claims, or that Radar couldn't search fully where it
	// could the object's: evidence that didn't settle the question, kept so
	// an answer can say why. When Via is a fallback to the nearest claim,
	// claims farther up the chain stay here.
	Between  []ManagerEvidence
	ChainEnd ChainEnd
	// Stop is the controller reference the walk couldn't follow, when
	// ChainEnd is unfollowed.
	Stop *ChainStop
	// Narrowed: the caller's access is restricted, so the evidence is the
	// part the caller may read.
	Narrowed bool
}

// ChainStop is a controller reference a walk up the chain couldn't follow.
type ChainStop struct {
	// At is the last object the walk reached, whose reference it is.
	At resourceid.Ref
	// Reference is At's controller reference, as At's metadata has it.
	Reference metav1.OwnerReference
	Owner     OwnerResolution
}

// Manager says what manages ref: the local managers whose lists name it and
// the claims its metadata makes, or, when those don't settle it, the same of
// its nearest controller ancestor that does (see Via). Evidence that
// disagrees stays a disagreement. The answer means something only when
// Subject is Present; otherwise its State is empty and Subject says why.
func (v *View) Manager(ref resourceid.Ref) ManagerAnswer {
	chain := v.ControllerChain(ref)
	a := ManagerAnswer{Subject: chain.Subject, Narrowed: v.access != Unrestricted()}
	if !chain.Subject.Present() {
		return a
	}
	complete := true
	for i, r := range chain.Chain {
		e := v.managerEvidence(r)
		if i == 0 {
			a.Direct = e
		}
		if e.strong() {
			if i > 0 {
				a.Via = &e
			}
			a.State, a.Lead = derive(e, e.complete())
			return a
		}
		complete = complete && e.complete()
		if i > 0 && (len(e.Claims) > 0 || !e.complete() && a.Direct.complete()) {
			a.Between = append(a.Between, e)
		}
	}
	switch {
	case chain.UnfollowedReference != nil:
		a.ChainEnd = ChainEndUnfollowed
		at := chain.Chain[len(chain.Chain)-1]
		a.Stop = &ChainStop{At: at, Owner: *chain.UnfollowedReference}
		if read, _ := v.read(at); read.State == Present {
			if i := slices.IndexFunc(read.Object.OwnerReferences, isController); i >= 0 {
				a.Stop.Reference = read.Object.OwnerReferences[i]
			}
		}
	case chain.Cycle:
		a.ChainEnd = ChainEndCycle
	case chain.Truncated:
		a.ChainEnd = ChainEndTruncated
	}
	// No object settled it. A claim the walk passed is still the best lead
	// it found, unless the object's own metadata makes one.
	if !slices.ContainsFunc(a.Direct.Claims, active) {
		if i := slices.IndexFunc(a.Between, func(e ManagerEvidence) bool { return slices.ContainsFunc(e.Claims, active) }); i >= 0 {
			e := a.Between[i]
			a.Via, a.Between = &e, slices.Delete(slices.Clone(a.Between), i, i+1)
			a.State, a.Lead = derive(e, chainSettled(a) && complete)
			return a
		}
	}
	a.State, a.Lead = derive(a.Direct, chainSettled(a) && complete)
	return a
}

// active reports whether c counts as a claim: one copied from another object
// names that object's manager.
func active(c CheckedClaim) bool { return !c.Copied }

// chainSettled reports whether the walk read the whole chain: it reached the
// top, or an owner that is gone, was replaced or is an Event, which ends the
// chain with an answer rather than leaving a manager above it unread.
func chainSettled(a ManagerAnswer) bool {
	switch a.ChainEnd {
	case "":
		return true
	case ChainEndUnfollowed:
		switch a.Stop.Owner.State {
		case OwnerGone, OwnerReplaced, OwnerExcluded:
			return true
		}
	}
	return false
}

// managerEvidence reads the evidence on ref, an object the view holds.
func (v *View) managerEvidence(ref resourceid.Ref) ManagerEvidence {
	read, _ := v.read(ref)
	found := v.RecordingManagers(ref)
	e := ManagerEvidence{Object: ref, Unreadable: len(found.Unreadable), Remote: len(found.Remote), Gaps: found.Gaps, Undecided: found.Undecided}
	for _, m := range found.Managers {
		e.Recorders = append(e.Recorders, m.Ref)
	}
	controlled := slices.ContainsFunc(read.Object.OwnerReferences, isController)
	for _, c := range read.Object.Claims {
		cc := CheckedClaim{Claim: c}
		cc.Check = v.checkClaim(c.Manager, e.Recorders, found)
		cc.Weak = c.Copied || controlled && inherited(c.Source, cc.Check)
		e.Claims = append(e.Claims, cc)
	}
	return e
}

// inherited reports whether a claim from source, checked as check, is metadata
// a controller copied onto an object it created. Argo CD instance labels and
// Helm annotations come from a template or a controller's own metadata. Flux
// origin labels are copied the same way, but Flux also labels what it applies,
// which a controller may later adopt; they count as copied only when the
// manager they name reads as not recording the object.
func inherited(source gitops.ClaimSource, check ClaimCheck) bool {
	switch source {
	case gitops.ClaimArgoInstanceLabel, gitops.ClaimHelmRelease:
		return true
	case gitops.ClaimFluxKustomization, gitops.ClaimFluxHelmRelease:
		return check == ClaimOmitted || check == ClaimMissing
	}
	return false
}

func (v *View) checkClaim(claimed resourceid.Ref, recorders []resourceid.Ref, found RecordingManagersAnswer) ClaimCheck {
	if i := slices.IndexFunc(recorders, func(r resourceid.Ref) bool { return names(claimed, r) }); i >= 0 {
		return ClaimRecorded
	}
	switch {
	case !IsRecordingManager(claimed.GroupKind()):
		return ClaimNotRecording
	case claimed.Namespace == "":
		return ClaimUnresolved
	case slices.Contains(found.Unreadable, claimed):
		return ClaimUnreadable
	case slices.Contains(found.Remote, claimed):
		return ClaimRemote
	}
	subject := v.subject(claimed)
	switch {
	case subject.Access == Denied:
		return ClaimWithheld
	case subject.Read.State == Absent:
		return ClaimMissing
	case !subject.Present():
		return ClaimUnchecked
	}
	// Only the list's state matters here: building its members would cost
	// as much as a large Application lists.
	members, checked := v.claimMembership[claimed]
	if !checked {
		members = v.session.Membership(claimed)
		v.claimMembership[claimed] = members
	}
	if members.Manager.State != Present || members.Manager.Object.UID != subject.Read.Object.UID {
		return ClaimUnchecked
	}
	switch members.Membership {
	case MembersRemote:
		return ClaimRemote
	case MembersUnreadable:
		return ClaimUnreadable
	case MembersRecorded:
		if found.Searched(claimed) {
			return ClaimOmitted
		}
	}
	return ClaimUnchecked
}

// names reports whether a claim of claimed names manager: the same manager,
// or, for a claim without a namespace, one of that kind and name.
func names(claimed, manager resourceid.Ref) bool {
	if claimed.Namespace == "" {
		claimed.Namespace = manager.Namespace
	}
	return claimed == manager
}

// derive sums up e. settled says whether nothing beyond e could name a
// manager, e itself or an ancestor: without it, finding no evidence is
// unknown rather than none.
func derive(e ManagerEvidence, settled bool) (ManagerState, *resourceid.Ref) {
	var active []gitops.Claim
	for _, c := range e.Claims {
		if !c.Copied {
			active = append(active, c.Claim)
		}
	}
	// A native Helm release names only how its chart was installed: Flux's
	// helm-controller and Argo CD's own installs carry its annotations too.
	if len(e.Recorders) > 0 || slices.ContainsFunc(active, func(c gitops.Claim) bool { return c.Source != gitops.ClaimHelmRelease }) {
		active = slices.DeleteFunc(active, func(c gitops.Claim) bool { return c.Source == gitops.ClaimHelmRelease })
	}
	named := func(manager resourceid.Ref) bool {
		return slices.ContainsFunc(active, func(c gitops.Claim) bool { return names(c.Manager, manager) })
	}
	switch {
	case len(e.Recorders) > 1:
		claimed := slices.DeleteFunc(slices.Clone(e.Recorders), func(r resourceid.Ref) bool { return !named(r) })
		if len(claimed) == 1 {
			return ManagerShared, &claimed[0]
		}
		return ManagerShared, nil
	case len(e.Recorders) == 1:
		r := e.Recorders[0]
		switch {
		case slices.ContainsFunc(active, func(c gitops.Claim) bool { return !names(c.Manager, r) }):
			return ManagerDisagree, nil
		case len(active) > 0:
			return ManagerAgree, &r
		}
		return ManagerListed, &r
	case len(active) > 0:
		// Claims name one manager when each names the lead, a claim without a
		// namespace naming any of its kind and name; the lead is the one that
		// says its namespace.
		lead := active[0].Manager
		for _, c := range active {
			if c.Manager.Namespace != "" {
				lead = c.Manager
				break
			}
		}
		if slices.ContainsFunc(active, func(c gitops.Claim) bool { return !names(c.Manager, lead) }) {
			return ManagerClaimed, nil
		}
		return ManagerClaimed, &lead
	case !settled:
		return ManagerUnknown, nil
	}
	return ManagerNone, nil
}