Ctrl K
ComunidadGuías…

pkg/ownership/cachesource/membership.go

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

Archivo original ↗Preguntar a KiBot
package cachesource

import (
	"slices"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/types"

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

// recordedMembers is what a manager's status says about its members.
type recordedMembers struct {
	state ownership.MembershipState
	gitops.MemberList
}

func membersOf(gk resourceid.GroupKind, u *unstructured.Unstructured) recordedMembers {
	var list gitops.MemberList
	var ok bool
	switch gk {
	case ownership.ArgoApplication:
		list, ok = gitops.ArgoMembers(u)
	case ownership.FluxHelmRelease, ownership.FluxKustomization:
		list, ok = gitops.FluxMembers(u)
	}
	switch {
	case !ok && appliesElsewhere(gk, u):
		return recordedMembers{state: ownership.MembersRemote}
	case !ok:
		return recordedMembers{}
	}
	return recordedMembers{state: located(gk, u), MemberList: list}
}

// memberState is membersOf's state alone. A search reads it for every manager
// whose list names the object; building that list would cost as much as the
// list is long.
func memberState(gk resourceid.GroupKind, u *unstructured.Unstructured) ownership.MembershipState {
	var ok bool
	switch gk {
	case ownership.ArgoApplication:
		ok = gitops.ArgoMembersReadable(u)
	case ownership.FluxHelmRelease, ownership.FluxKustomization:
		ok = gitops.FluxMembersReadable(u)
	}
	switch {
	case !ok && appliesElsewhere(gk, u):
		return ownership.MembersRemote
	case !ok:
		return ownership.MembersUnreadable
	}
	return located(gk, u)
}

type membershipStateObservation struct {
	uid                 types.UID
	version, apiVersion string
	state               ownership.MembershipState
}

// A resource version identifies the immutable inventory read from the informer.
// Keep only its scalar validation result, not the potentially large cached object.
func (s *session) cachedMemberState(gk resourceid.GroupKind, u *unstructured.Unstructured) ownership.MembershipState {
	if u.GetResourceVersion() == "" {
		return memberState(gk, u)
	}
	ref := resourceid.Ref{Group: gk.Group, Kind: gk.Kind, Namespace: u.GetNamespace(), Name: u.GetName()}
	previous, found := s.membershipStates[ref]
	if found && previous.uid == u.GetUID() && previous.version == u.GetResourceVersion() && previous.apiVersion == u.GetAPIVersion() {
		return previous.state
	}
	state := memberState(gk, u)
	if s.membershipStates == nil {
		s.membershipStates = make(map[resourceid.Ref]membershipStateObservation)
	}
	s.membershipStates[ref] = membershipStateObservation{uid: u.GetUID(), version: u.GetResourceVersion(), apiVersion: u.GetAPIVersion(), state: state}
	return state
}

// appliesElsewhere reports whether a manager is known to apply to another
// cluster, whose objects alone its list can name, whether Radar can read the
// list or not.
func appliesElsewhere(gk resourceid.GroupKind, u *unstructured.Unstructured) bool {
	if gk == ownership.ArgoApplication {
		return gitops.ArgoAppliesElsewhere(u)
	}
	return !gitops.FluxTargetsLocalCluster(u)
}

// located is whether a manager whose list Radar reads applies it here.
func located(gk resourceid.GroupKind, u *unstructured.Unstructured) ownership.MembershipState {
	if appliesHere(gk, u) {
		return ownership.MembersRecorded
	}
	return ownership.MembersRemote
}

// appliesHere reports whether a manager's member list names objects of this
// cluster: an Argo CD list compared at the in-cluster destination, or a Flux
// object without spec.kubeConfig.
func appliesHere(gk resourceid.GroupKind, u *unstructured.Unstructured) bool {
	if gk == ownership.ArgoApplication {
		name, server, _ := gitops.ArgoComparedDestination(u)
		return gitops.IsInClusterArgoDestination(name, server)
	}
	return gitops.FluxTargetsLocalCluster(u)
}

func (s *session) Members(manager resourceid.Ref) ownership.MembersRead {
	gk := manager.GroupKind()
	if !ownership.IsRecordingManager(gk) {
		return ownership.MembersRead{Manager: unavailable(manager, readreason.ReadError)}
	}
	var members recordedMembers
	read := s.readObject(manager, func(u *unstructured.Unstructured) { members = membersOf(gk, u) })
	r := ownership.MembersRead{Manager: read}
	if read.State != ownership.Present {
		return r
	}
	r.Membership = members.state
	if members.state == ownership.MembersUnreadable {
		return r
	}
	r.Members = make([]resourceid.Ref, 0, len(members.Entries))
	for _, e := range members.Entries {
		r.Members = append(r.Members, s.align(e, members.DestinationNamespace))
	}
	slices.SortFunc(r.Members, compareRefs)
	r.Members = slices.Compact(r.Members)
	return r
}

func (s *session) Membership(manager resourceid.Ref) ownership.MembersRead {
	gk := manager.GroupKind()
	if !ownership.IsRecordingManager(gk) {
		return ownership.MembersRead{Manager: unavailable(manager, readreason.ReadError)}
	}
	var state ownership.MembershipState
	read := s.readObject(manager, func(u *unstructured.Unstructured) { state = s.cachedMemberState(gk, u) })
	r := ownership.MembersRead{Manager: read}
	if read.State == ownership.Present {
		r.Membership = state
	}
	return r
}

// builtinKind returns the built-in kind group/kind names, if it is one.
func builtinKind(group, kind string) (resourceid.Builtin, bool) {
	b, ok := resourceid.BuiltinForKind(kind)
	return b, ok && b.Group == group
}

// align reads an entry as its kind's scope says, or as recorded when Radar
// can't tell the scope.
func (s *session) align(e gitops.Member, destination string) resourceid.Ref {
	ref := resourceid.Ref{Group: e.Group, Kind: e.Kind, Namespace: e.Namespace, Name: e.Name}
	if namespaced, known := s.catalog.Scope(ref.GroupKind()); known {
		ref.Namespace = gitops.MemberNamespace(namespaced, e.Namespace, destination)
	}
	return ref
}

// placements returns the GitOpsMembersIndex keys under which an entry naming ref
// is filed, as its kind's scope says, or why no entry can name ref.
func (s *session) placements(ref resourceid.Ref) ([]string, readreason.Code) {
	if ref.Kind == "" || ref.Name == "" {
		return nil, readreason.ReadError
	}
	gk := ref.GroupKind()
	namespaced, known := s.catalog.Scope(gk)
	_, builtin := builtinKind(ref.Group, ref.Kind)
	switch {
	case !known && (builtin || s.catalog.Ambiguous(gk) || !s.catalog.Complete()):
		// Radar can't tell the kind's scope.
		return nil, readreason.ReadError
	case !known:
		// No object of the kind can exist; entries name it as recorded.
		return []string{k8score.MemberKey(k8score.MemberAsRecorded, ref.Group, ref.Kind, ref.Namespace, ref.Name)}, ""
	case namespaced && ref.Namespace == "":
		return nil, readreason.NamespaceRequired
	case !namespaced && ref.Namespace != "":
		return nil, readreason.ReadError
	case builtin:
		return []string{k8score.MemberKey(k8score.MemberMeant, ref.Group, ref.Kind, ref.Namespace, ref.Name)}, ""
	case namespaced:
		return []string{k8score.MemberKey(k8score.MemberAsRecorded, ref.Group, ref.Kind, ref.Namespace, ref.Name), k8score.MemberKey(k8score.MemberInDestination, ref.Group, ref.Kind, ref.Namespace, ref.Name)}, ""
	}
	return []string{k8score.MemberKey(k8score.MemberAsRecorded, ref.Group, ref.Kind, "", ref.Name), k8score.MemberKey(k8score.MemberClusterScoped, ref.Group, ref.Kind, "", ref.Name)}, ""
}

func (s *session) RecordingManagers(ref resourceid.Ref) ownership.RecordingManagersRead {
	keys, reason := s.placements(ref)
	if reason != "" {
		var r ownership.RecordingManagersRead
		for _, gk := range ownership.RecordingManagerKinds() {
			r.Gaps = append(r.Gaps, ownership.Gap{Kind: gk, Reason: reason})
		}
		slices.SortFunc(r.Gaps, compareGaps)
		return r
	}
	q := &search{scope: ownership.Everywhere()}
	var managers []candidate
	var remote, unreadable []resourceid.Ref
	for _, gk := range ownership.RecordingManagerKinds() {
		kind, served := s.catalog.Kind(gk)
		if !served {
			// Not installed, unless discovery missed it.
			if !s.catalog.Complete() || s.catalog.Ambiguous(gk) {
				q.gap(gk, "", readreason.ReadError)
			}
			continue
		}
		m := s.readMembers(kind, keys)
		managers = append(managers, m.managers...)
		remote = append(remote, m.remote...)
		unreadable = append(unreadable, m.unreadable...)
		q.gaps = append(q.gaps, m.gaps...)
	}
	var r ownership.RecordingManagersRead
	slices.SortFunc(managers, func(a, b candidate) int { return compareRefs(a.ref, b.ref) })
	managers = slices.CompactFunc(managers, func(a, b candidate) bool { return a.ref == b.ref })
	for _, c := range managers {
		if read := observe(c.ref, c.obj); read.State == ownership.Present {
			r.Managers = append(r.Managers, read.Object)
		} else {
			q.gaps = append(q.gaps, read.Gap)
		}
	}
	slices.SortFunc(remote, compareRefs)
	r.Remote = slices.Compact(remote)
	slices.SortFunc(unreadable, compareRefs)
	r.Unreadable = slices.Compact(unreadable)
	slices.SortFunc(q.gaps, compareGaps)
	r.Gaps = slices.Compact(q.gaps)
	return r
}

// membersRead is what one manager kind's index reads found.
type membersRead struct {
	managers           []candidate
	remote, unreadable []resourceid.Ref
	gaps               []ownership.Gap
}

// readMembers reads the managers of kind filed under the unreadable key or any
// of keys, each store of kind once for all of them. A manager that changes
// while RecordingManagers runs, from an unreadable list to one naming the object or
// from one placement of its entry to another, is then found in one of its
// states: reads of one key at a time could find it in none.
func (s *session) readMembers(kind ownership.CatalogKind, keys []string) membersRead {
	gk := kind.GroupKind
	var m membersRead
	q := &search{scope: ownership.Everywhere()}
	keys = append([]string{k8score.UnreadableMembersKey}, keys...)
	s.dynamicIndexed(q, kind, k8score.GitOpsMembersIndex, keys, func(u *unstructured.Unstructured) {
		manager := resourceid.Ref{Group: gk.Group, Kind: gk.Kind, Namespace: u.GetNamespace(), Name: u.GetName()}
		switch s.cachedMemberState(gk, u) {
		case ownership.MembersUnreadable:
			m.unreadable = append(m.unreadable, manager)
		case ownership.MembersRemote:
			m.remote = append(m.remote, manager)
		default:
			m.managers = append(m.managers, candidate{ref: manager, obj: u})
		}
	})
	if s.recordingManagersRead != nil {
		s.recordingManagersRead(gk, keys)
	}
	m.gaps = q.gaps
	return m
}