pkg/gitops/members.go
Rama main · réplica de lectura del repositorio original.
package gitops
import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
// Member is one object a GitOps manager's status lists, as recorded.
type Member struct {
Group, Kind, Namespace, Name string
}
// MemberList is what a GitOps manager's status records: for an Argo CD
// Application, the objects its last comparison covered, including desired
// objects not yet created, live ones awaiting pruning and ones another
// Application also tracks; for Flux, the inventory of its last apply. An
// entry says the manager recorded the object, not that it applied it or
// alone manages it.
type MemberList struct {
Entries []Member
// DestinationNamespace is the namespace of the destination an Argo CD
// Application's list was compared against, where an entry recording no
// namespace is; empty for Flux.
DestinationNamespace string
}
// ArgoMembers reads an Application's status.resources, as of the destination
// Argo CD compared it against. ok is false when the list can't be trusted, so
// that any object may be a member: the Application has no destination, the
// list is left out before Argo CD has reconciled it (reconciledAt unset), the
// list is empty or left out after a failed comparison, or an entry names no
// object. A nonempty list from a failed comparison is still what Argo CD
// recorded, though it may leave objects out.
func ArgoMembers(app *unstructured.Unstructured) (MemberList, bool) {
return argoMembers(app, true)
}
// ArgoMembersReadable reports whether ArgoMembers reads app's list, making the
// same checks without building the list, whose cost grows with it.
func ArgoMembersReadable(app *unstructured.Unstructured) bool {
_, ok := argoMembers(app, false)
return ok
}
func argoMembers(app *unstructured.Unstructured, collect bool) (list MemberList, ok bool) {
name, server, destination := ArgoComparedDestination(app)
if name == "" && server == "" {
return MemberList{}, false
}
list.DestinationNamespace = destination
raw, _, err := unstructured.NestedFieldNoCopy(app.Object, "status", "resources")
if err != nil {
return MemberList{}, false
}
if raw == nil {
// Argo CD omits an empty list, so once it has reconciled the
// Application, no list is an empty one.
if reconciled, _, _ := unstructured.NestedString(app.Object, "status", "reconciledAt"); reconciled == "" {
return MemberList{}, false
}
if ArgoComparisonFailed(app) {
return MemberList{}, false
}
return list, true
}
items, isList := raw.([]any)
if !isList {
return MemberList{}, false
}
if len(items) == 0 && ArgoComparisonFailed(app) {
return MemberList{}, false
}
if collect && len(items) > 0 {
list.Entries = make([]Member, 0, len(items))
}
for _, item := range items {
e, isMap := item.(map[string]any)
if !isMap {
return MemberList{}, false
}
kind, kindOK := e["kind"].(string)
name, nameOK := e["name"].(string)
group, groupOK := optionalString(e["group"])
namespace, namespaceOK := optionalString(e["namespace"])
if !kindOK || !nameOK || kind == "" || name == "" || !groupOK || !namespaceOK {
return MemberList{}, false
}
if collect {
list.Entries = append(list.Entries, Member{Group: group, Kind: kind, Namespace: namespace, Name: name})
}
}
return list, true
}
// FluxMembers reads a Kustomization's or HelmRelease's status.inventory. ok is
// false when there is no inventory or an entry's ID doesn't parse. Flux
// records no destination with its inventory, so after a spec.kubeConfig
// change the inventory lists the previous destination's objects until a
// reconcile applies the change.
func FluxMembers(obj *unstructured.Unstructured) (MemberList, bool) {
return fluxMembers(obj, true)
}
// FluxMembersReadable reports whether FluxMembers reads obj's list, making the
// same checks without building the list, whose cost grows with it.
func FluxMembersReadable(obj *unstructured.Unstructured) bool {
_, ok := fluxMembers(obj, false)
return ok
}
func fluxMembers(obj *unstructured.Unstructured, collect bool) (list MemberList, ok bool) {
raw, _, err := unstructured.NestedFieldNoCopy(obj.Object, "status", "inventory")
inventory, isMap := raw.(map[string]any)
if err != nil || !isMap {
return MemberList{}, false
}
items, isList := inventory["entries"].([]any)
if !isList && inventory["entries"] != nil {
return MemberList{}, false
}
if collect && len(items) > 0 {
list.Entries = make([]Member, 0, len(items))
}
for _, item := range items {
e, isMap := item.(map[string]any)
if !isMap {
return MemberList{}, false
}
id, isString := e["id"].(string)
if !isString {
return MemberList{}, false
}
if !collect {
if _, _, _, _, parsed := fluxInventoryParts(id); !parsed {
return MemberList{}, false
}
continue
}
group, kind, namespace, name, parsed := ParseFluxInventoryID(id)
if !parsed {
return MemberList{}, false
}
list.Entries = append(list.Entries, Member{Group: group, Kind: kind, Namespace: namespace, Name: name})
}
return list, true
}
// MemberNamespace is the namespace of the object an entry recording namespace
// means, for a kind of known scope: none for a cluster-scoped kind, and the
// destination for a namespaced one recorded without a namespace.
func MemberNamespace(namespaced bool, namespace, destination string) string {
switch {
case !namespaced:
return ""
case namespace == "":
return destination
}
return namespace
}
// optionalString reads a field that is either absent or a string.
func optionalString(v any) (string, bool) {
if v == nil {
return "", true
}
s, ok := v.(string)
return s, ok
}
// ArgoComparisonFailed reports whether sync status is Unknown or any condition
// has type ComparisonError. Argo can also add ComparisonError beside a current
// render, for example for a health assessment error. A comparison error can
// leave an empty, partial or previous resource list, so the list alone does not
// establish the current render or complete Application coverage.
func ArgoComparisonFailed(app *unstructured.Unstructured) bool {
if status, _, _ := unstructured.NestedString(app.Object, "status", "sync", "status"); status == "Unknown" {
return true
}
conditions, _, _ := unstructured.NestedSlice(app.Object, "status", "conditions")
for _, c := range conditions {
if m, ok := c.(map[string]any); ok && m["type"] == "ComparisonError" {
return true
}
}
return false
}