pkg/ownership/cachesource/membership_test.go
Rama main · réplica de lectura del repositorio original.
package cachesource
import (
"fmt"
"runtime"
"slices"
"testing"
"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/ownership/ownershiptest"
"github.com/skyhook-io/radar/pkg/readreason"
"github.com/skyhook-io/radar/pkg/resourceid"
)
func manager(gk resourceid.GroupKind, spec, status map[string]any) *unstructured.Unstructured {
u := &unstructured.Unstructured{Object: map[string]any{"metadata": map[string]any{"namespace": "ops", "name": "m"}}}
if spec != nil {
u.Object["spec"] = spec
}
if status != nil {
u.Object["status"] = status
}
u.SetKind(gk.Kind)
return u
}
func atGeneration(u *unstructured.Unstructured, generation int64) *unstructured.Unstructured {
u.SetGeneration(generation)
return u
}
var localDestination = map[string]any{"destination": map[string]any{"namespace": "shop", "server": "https://kubernetes.default.svc"}}
// The status shapes Argo CD 3 and Flux 2.8 controllers write, read entry by
// entry; a list Radar can't trust in full is unreadable as a whole.
func TestMembersReadTheShapesControllersWrite(t *testing.T) {
app, ks, hr := ownership.ArgoApplication, ownership.FluxKustomization, ownership.FluxHelmRelease
for _, tc := range []struct {
name string
obj *unstructured.Unstructured
state ownership.MembershipState
want []gitops.Member
}{
{"Argo CD entries, the core group left out", manager(app, localDestination, map[string]any{
"reconciledAt": "2026-10-17T01:37:54Z",
"resources": []any{
map[string]any{"kind": "Service", "name": "ui", "namespace": "shop", "status": "Synced", "version": "v1"},
map[string]any{"group": "apps", "kind": "Deployment", "name": "ui", "namespace": "shop", "status": "Synced", "version": "v1"},
map[string]any{"group": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "reader", "status": "Synced", "version": "v1"},
},
}), ownership.MembersRecorded, []gitops.Member{{Kind: "Service", Namespace: "shop", Name: "ui"}, {Group: "apps", Kind: "Deployment", Namespace: "shop", Name: "ui"}, {Group: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "reader"}}},
{"Argo CD, reconciled with nothing to list", manager(app, localDestination, map[string]any{"reconciledAt": "2026-10-17T01:37:54Z"}), ownership.MembersRecorded, nil},
{"Argo CD, never reconciled", manager(app, localDestination, nil), ownership.MembersUnreadable, nil},
{"Argo CD, an explicit null list before reconciling", manager(app, localDestination, map[string]any{"resources": nil}), ownership.MembersUnreadable, nil},
{"Argo CD, an entry without a name", manager(app, localDestination, map[string]any{"reconciledAt": "x", "resources": []any{map[string]any{"kind": "Service"}}}), ownership.MembersUnreadable, nil},
{"Argo CD, an entry's namespace not a string", manager(app, localDestination, map[string]any{"reconciledAt": "x", "resources": []any{map[string]any{"kind": "Service", "name": "ui", "namespace": int64(3)}}}), ownership.MembersUnreadable, nil},
{"Argo CD, resources not a list", manager(app, localDestination, map[string]any{"reconciledAt": "x", "resources": "ui"}), ownership.MembersUnreadable, nil},
{"Argo CD, the in-cluster destination by name", manager(app, map[string]any{"destination": map[string]any{"name": "in-cluster", "namespace": "shop"}}, map[string]any{"resources": []any{}}), ownership.MembersRecorded, nil},
{"Argo CD, another cluster", manager(app, map[string]any{"destination": map[string]any{"server": "https://prod.example.com:6443"}}, map[string]any{"resources": []any{map[string]any{"kind": "Service", "name": "ui"}}}), ownership.MembersRemote, []gitops.Member{{Kind: "Service", Name: "ui"}}},
// A manager applying to another cluster names nothing here, whether or
// not its list can be read.
{"Argo CD, another cluster, never reconciled", manager(app, map[string]any{"destination": map[string]any{"name": "prod"}}, nil), ownership.MembersRemote, nil},
{"Argo CD, another cluster, a failed comparison", manager(app, map[string]any{"destination": map[string]any{"server": "https://prod.example.com:6443"}}, map[string]any{"reconciledAt": "x", "conditions": []any{map[string]any{"type": "ComparisonError"}}}), ownership.MembersRemote, nil},
{"Argo CD, no destination", manager(app, map[string]any{"destination": map[string]any{}}, map[string]any{"resources": []any{}}), ownership.MembersUnreadable, nil},
{"a Kustomization's inventory", manager(ks, map[string]any{"prune": true, "targetNamespace": "shop"}, map[string]any{"inventory": map[string]any{"entries": []any{
map[string]any{"id": "shop_podinfo__Service", "v": "v1"},
map[string]any{"id": "shop_podinfo_apps_Deployment", "v": "v1"},
map[string]any{"id": "_reader_rbac.authorization.k8s.io_ClusterRole", "v": "v1"},
}}}), ownership.MembersRecorded, []gitops.Member{{Kind: "Service", Namespace: "shop", Name: "podinfo"}, {Group: "apps", Kind: "Deployment", Namespace: "shop", Name: "podinfo"}, {Group: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "reader"}}},
{"a HelmRelease's inventory", manager(hr, map[string]any{"releaseName": "web"}, map[string]any{"inventory": map[string]any{"entries": []any{
map[string]any{"id": "shop_web-podinfo__Service", "v": "v1"},
}}}), ownership.MembersRecorded, []gitops.Member{{Kind: "Service", Namespace: "shop", Name: "web-podinfo"}}},
{"RBAC names with colons, as Flux writes them", manager(hr, map[string]any{"releaseName": "metrics-server"}, map[string]any{"inventory": map[string]any{"entries": []any{
map[string]any{"id": "_system__metrics-server_rbac.authorization.k8s.io_ClusterRole", "v": "v1"},
map[string]any{"id": "kube-system_metrics-server__system__auth-delegator_rbac.authorization.k8s.io_RoleBinding", "v": "v1"},
}}}), ownership.MembersRecorded, []gitops.Member{{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "system:metrics-server"}, {Group: "rbac.authorization.k8s.io", Kind: "RoleBinding", Namespace: "kube-system", Name: "metrics-server:system:auth-delegator"}}},
{"an empty inventory", manager(ks, nil, map[string]any{"inventory": map[string]any{"entries": []any{}}}), ownership.MembersRecorded, nil},
{"an inventory without entries", manager(ks, nil, map[string]any{"inventory": map[string]any{}}), ownership.MembersRecorded, nil},
{"a HelmRelease from a controller that records no inventory", manager(hr, map[string]any{"releaseName": "web"}, map[string]any{"observedGeneration": int64(2)}), ownership.MembersUnreadable, nil},
{"an ID that doesn't parse", manager(ks, nil, map[string]any{"inventory": map[string]any{"entries": []any{map[string]any{"id": "podinfo", "v": "v1"}}}}), ownership.MembersUnreadable, nil},
{"entries not a list", manager(ks, nil, map[string]any{"inventory": map[string]any{"entries": "podinfo"}}), ownership.MembersUnreadable, nil},
{"a Kustomization applying elsewhere", manager(ks, map[string]any{"kubeConfig": map[string]any{"secretRef": map[string]any{"name": "prod"}}}, map[string]any{"inventory": map[string]any{"entries": []any{}}}), ownership.MembersRemote, nil},
{"a HelmRelease applying elsewhere", manager(hr, map[string]any{"kubeConfig": map[string]any{"secretRef": map[string]any{"name": "prod"}}}, map[string]any{"inventory": map[string]any{"entries": []any{
map[string]any{"id": "shop_web-podinfo__Service", "v": "v1"},
}}}), ownership.MembersRemote, []gitops.Member{{Kind: "Service", Namespace: "shop", Name: "web-podinfo"}}},
{"a HelmRelease applying elsewhere, without an inventory", manager(hr, map[string]any{"kubeConfig": map[string]any{"secretRef": map[string]any{"name": "prod"}}}, nil), ownership.MembersRemote, nil},
{"a Kustomization whose spec changed since its inventory", atGeneration(manager(ks, nil, map[string]any{"observedGeneration": int64(1), "inventory": map[string]any{"entries": []any{
map[string]any{"id": "shop_ui__Service", "v": "v1"},
}}}), 2), ownership.MembersRecorded, []gitops.Member{{Kind: "Service", Namespace: "shop", Name: "ui"}}},
} {
t.Run(tc.name, func(t *testing.T) {
gk := ownership.ArgoApplication
switch tc.obj.GetKind() {
case ks.Kind:
gk = ks
case hr.Kind:
gk = hr
}
got := membersOf(gk, tc.obj)
if got.state != tc.state || !slices.Equal(got.Entries, tc.want) {
t.Fatalf("got %v %v, want %v %v", got.state, got.Entries, tc.state, tc.want)
}
if state := memberState(gk, tc.obj); state != tc.state {
t.Fatalf("memberState = %v, membersOf %v", state, tc.state)
}
})
}
}
// RecordingManagers is Members inverted: over a fully collected World, every member
// a manager lists names it, as a recorder or, for a remote manager, as
// elsewhere; and every manager so named lists the member. This holds of the
// source whatever the oracle says.
func TestRecordingManagersInvertMembers(t *testing.T) {
worlds := map[string]ownershiptest.World{"fixture": ownershiptest.Fixture()}
for seed := range uint64(10) {
w := ownershiptest.Generate(seed)
w.Partial = false
for gk, c := range w.Collection {
if c.State != ownershiptest.Unwatchable {
w.Collection[gk] = ownershiptest.Collection{OnDemand: c.OnDemand}
}
}
worlds[fmt.Sprint("generated ", seed)] = w
}
for name, w := range worlds {
t.Run(name, func(t *testing.T) {
s := begin(t, render(t, w).source())
q := ownershiptest.QueriesFor(w)
listed := map[resourceid.Ref][]resourceid.Ref{}
remote := map[resourceid.Ref]bool{}
for _, m := range q.Managers {
inv := s.Members(m)
remote[m] = inv.Membership == ownership.MembersRemote
for _, member := range inv.Members {
listed[m] = append(listed[m], member)
rec := s.RecordingManagers(member)
named := slices.ContainsFunc(rec.Managers, func(o ownership.Observation) bool { return o.Ref == m })
if remote[m] {
named = slices.Contains(rec.Remote, m)
}
if !named && len(rec.Gaps) == 0 {
t.Errorf("%v (%v) lists %v, but RecordingManagers(%v) = %+v", m, inv.Membership, member, member, rec)
}
}
}
for _, ref := range q.Refs {
rec := s.RecordingManagers(ref)
var named []resourceid.Ref
for _, o := range rec.Managers {
named = append(named, o.Ref)
}
for _, m := range named {
if remote[m] || !slices.Contains(listed[m], ref) {
t.Errorf("RecordingManagers(%v) names %v as a recorder; its Members lists %v", ref, m, listed[m])
}
}
for _, m := range rec.Remote {
if !remote[m] || !slices.Contains(listed[m], ref) {
t.Errorf("RecordingManagers(%v) names %v as remote; its Members lists %v", ref, m, listed[m])
}
}
}
})
}
}
// A built-in kind's scope is its own, unless discovery is ambiguous about the
// kind or serves it with the other scope: then no entry names it for certain,
// and its entries stay as recorded. The oracle says the same.
func TestRecordingManagersOfABuiltInDiscoveryDisputes(t *testing.T) {
reader := resourceid.Ref{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "reader"}
clusterRole := reader.GroupKind()
gc := []string{"create", "delete", "get", "list", "patch", "update", "watch"}
for name, served := range map[string][]ownership.ServedResource{
"ambiguous": {
{Group: clusterRole.Group, Version: "v1", Kind: clusterRole.Kind, Resource: "clusterroles", Verbs: gc},
{Group: clusterRole.Group, Version: "v1", Kind: clusterRole.Kind, Resource: "tenantroles", Namespaced: true, Verbs: gc},
},
"served with the other scope": {
{Group: clusterRole.Group, Version: "v1", Kind: clusterRole.Kind, Resource: "clusterroles", Namespaced: true, Verbs: gc},
},
} {
t.Run(name, func(t *testing.T) {
w := ownershiptest.World{Resources: append(ownershiptest.Resources(), served...), Collection: map[resourceid.GroupKind]ownershiptest.Collection{
clusterRole: {State: ownershiptest.NotCollected},
}}
for _, k := range ownershiptest.Kinds {
w.Collection[k.GroupKind] = ownershiptest.Collection{OnDemand: !k.Typed}
}
app := ownershiptest.Object{
Ref: resourceid.Ref{Group: ownership.ArgoApplication.Group, Kind: ownership.ArgoApplication.Kind, Namespace: "argocd", Name: "rbac"},
APIVersion: "argoproj.io/v1alpha1", UID: "uid-rbac", ResourceVersion: "1",
Membership: ownership.MembersRecorded, Members: []resourceid.Ref{reader}, DestinationNamespace: "alpha",
}
w.Objects = []ownershiptest.Object{app}
scan, _ := ownershiptest.Scan(w).Begin()
cache := begin(t, render(t, w).source())
// The ClusterRole can't be named, so no manager kind was searched.
var want []ownership.Gap
for _, gk := range ownership.RecordingManagerKinds() {
want = append(want, ownership.Gap{Kind: gk, Reason: readreason.ReadError})
}
slices.SortFunc(want, compareGaps)
for source, s := range map[string]ownership.ReadSession{"scan": scan, "cache": cache} {
if got := s.RecordingManagers(reader); len(got.Managers) != 0 || !slices.Equal(got.Gaps, want) {
t.Errorf("%s: RecordingManagers(%v) = %+v, want every manager kind unread: %+v", source, reader, got, want)
}
if got := s.Members(app.Ref); got.Membership != ownership.MembersRecorded || !slices.Equal(got.Members, []resourceid.Ref{reader}) {
t.Errorf("%s: Members = %v %v, want the entry as recorded", source, got.Membership, got.Members)
}
}
})
}
}
// A manager kind two resources of its group serve can't be read as either, so
// a manager of that kind may record the object unseen.
func TestRecordingManagersLeaveAnAmbiguousManagerKindUnread(t *testing.T) {
w := ownershiptest.World{Resources: ownershiptest.Resources(), Collection: map[resourceid.GroupKind]ownershiptest.Collection{}}
for _, k := range ownershiptest.Kinds {
w.Collection[k.GroupKind] = ownershiptest.Collection{OnDemand: !k.Typed}
}
w.Resources = append(w.Resources, ownership.ServedResource{Group: ownership.ArgoApplication.Group, Version: "v1alpha1", Kind: ownership.ArgoApplication.Kind,
Resource: "otherapplications", Namespaced: true, Verbs: []string{"delete", "get", "list", "watch"}})
got := begin(t, render(t, w).source()).RecordingManagers(resourceid.Ref{Kind: "ConfigMap", Namespace: "alpha", Name: "cfg"})
if want := (ownership.Gap{Kind: ownership.ArgoApplication, Reason: readreason.ReadError}); !slices.Contains(got.Gaps, want) || got.Complete() {
t.Fatalf("RecordingManagers = %+v, complete %v; want the gap %+v", got, got.Complete(), want)
}
}
// An answer is complete only when no remote manager may record the object
// and no manager's list is unreadable.
func TestRecordingManagersAreCompleteOnlyWithoutDoubt(t *testing.T) {
cfg := resourceid.Ref{Kind: "ConfigMap", Namespace: "alpha", Name: "cfg"}
app := func(name string, state ownership.MembershipState, members ...resourceid.Ref) ownershiptest.Object {
return ownershiptest.Object{
Ref: resourceid.Ref{Group: ownership.ArgoApplication.Group, Kind: ownership.ArgoApplication.Kind, Namespace: "argocd", Name: name},
APIVersion: "argoproj.io/v1alpha1", UID: types.UID("uid-" + name), ResourceVersion: "1",
Membership: state, Members: members, DestinationNamespace: "alpha",
}
}
for _, tc := range []struct {
name string
managers []ownershiptest.Object
complete bool
}{
{"a local recorder", []ownershiptest.Object{app("local", ownership.MembersRecorded, cfg)}, true},
{"a remote manager listing it too", []ownershiptest.Object{app("local", ownership.MembersRecorded, cfg), app("prod", ownership.MembersRemote, cfg)}, false},
{"a remote manager listing something else", []ownershiptest.Object{app("local", ownership.MembersRecorded, cfg), app("prod", ownership.MembersRemote)}, true},
{"an unreadable manager", []ownershiptest.Object{app("local", ownership.MembersRecorded, cfg), app("new", ownership.MembersUnreadable)}, false},
} {
t.Run(tc.name, func(t *testing.T) {
w := ownershiptest.World{Resources: ownershiptest.Resources(), Collection: map[resourceid.GroupKind]ownershiptest.Collection{}, Objects: tc.managers}
for _, k := range ownershiptest.Kinds {
w.Collection[k.GroupKind] = ownershiptest.Collection{OnDemand: !k.Typed}
}
got := begin(t, render(t, w).source()).RecordingManagers(cfg)
if got.Complete() != tc.complete || len(got.Managers) != 1 {
t.Fatalf("RecordingManagers = %+v: complete %v, want %v", got, got.Complete(), tc.complete)
}
})
}
}
// Flux writes a colon in an RBAC object's name as a double underscore; its
// entry still names the object the API does, which a chart's ClusterRoles
// such as system:metrics-server need to be found at all.
func TestMembersOfRBACNamesWithColons(t *testing.T) {
role := resourceid.Ref{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "system:metrics-server"}
release := ownershiptest.Object{
Ref: resourceid.Ref{Group: ownership.FluxHelmRelease.Group, Kind: ownership.FluxHelmRelease.Kind, Namespace: "kube-system", Name: "metrics-server"},
APIVersion: "helm.toolkit.fluxcd.io/v2", UID: "uid-metrics-server", ResourceVersion: "1",
Membership: ownership.MembersRecorded, Members: []resourceid.Ref{role},
}
w := ownershiptest.World{Resources: ownershiptest.Resources(), Collection: map[resourceid.GroupKind]ownershiptest.Collection{}, Objects: []ownershiptest.Object{release}}
for _, k := range ownershiptest.Kinds {
w.Collection[k.GroupKind] = ownershiptest.Collection{OnDemand: !k.Typed}
}
s := begin(t, render(t, w).source())
if got := s.Members(release.Ref); !slices.Equal(got.Members, []resourceid.Ref{role}) {
t.Fatalf("Members = %v, want %v", got.Members, role)
}
got := s.RecordingManagers(role)
if len(got.Managers) != 1 || got.Managers[0].Ref != release.Ref || !got.Complete() {
t.Fatalf("RecordingManagers(%v) = %+v, want %v alone", role, got, release.Ref)
}
}
// RecordingManagers reads each manager kind once, asking for the unreadable key and
// every placement of the object in the same read. That one read takes all of them
// in one pass under the store's lock (TestReadableIndexedReadsItsKeysTogether),
// so a manager changing during the search, from an unreadable list to one
// naming the object or from one placement of its entry to another, is found in
// one of its states; one read per key could land between two changes and find
// it in none.
func TestRecordingManagersReadEachManagerKindOnce(t *testing.T) {
s := begin(t, render(t, ownershiptest.Fixture()).source()).(*session)
reads := map[resourceid.GroupKind][][]string{}
s.recordingManagersRead = func(kind resourceid.GroupKind, keys []string) {
reads[kind] = append(reads[kind], slices.Clone(keys))
}
s.RecordingManagers(resourceid.Ref{Group: "example.io", Kind: "Widget", Namespace: "alpha", Name: "w1"})
want := []string{
k8score.UnreadableMembersKey,
k8score.MemberKey(k8score.MemberAsRecorded, "example.io", "Widget", "alpha", "w1"),
k8score.MemberKey(k8score.MemberInDestination, "example.io", "Widget", "alpha", "w1"),
}
for _, gk := range ownership.RecordingManagerKinds() {
if got := reads[gk]; len(got) != 1 || !slices.Equal(got[0], want) {
t.Errorf("%v read %q, want one read of %q", gk, got, want)
}
}
}
// A search classifies every manager whose list names the object. Opening a
// member of a 10,000-object Application or Kustomization must not cost what
// building its list would.
func TestMemberStateCostDoesNotGrowWithTheList(t *testing.T) {
argo := func(n int) *unstructured.Unstructured {
entries := make([]any, n)
for i := range entries {
entries[i] = map[string]any{"kind": "Service", "name": fmt.Sprint("svc-", i), "namespace": "shop"}
}
return manager(ownership.ArgoApplication, localDestination, map[string]any{"reconciledAt": "x", "resources": entries})
}
flux := func(n int) *unstructured.Unstructured {
entries := make([]any, n)
for i := range entries {
entries[i] = map[string]any{"id": fmt.Sprintf("shop_svc-%d__Service", i), "v": "v1"}
}
return manager(ownership.FluxKustomization, nil, map[string]any{"inventory": map[string]any{"entries": entries}})
}
for _, tc := range []struct {
gk resourceid.GroupKind
small, large *unstructured.Unstructured
}{
{ownership.ArgoApplication, argo(40), argo(10000)},
{ownership.FluxKustomization, flux(40), flux(10000)},
} {
if state := memberState(tc.gk, tc.large); state != ownership.MembersRecorded {
t.Fatalf("%v: %v", tc.gk, state)
}
small := testing.AllocsPerRun(20, func() { memberState(tc.gk, tc.small) })
large := testing.AllocsPerRun(20, func() { memberState(tc.gk, tc.large) })
if large > small {
t.Errorf("%v: %v allocations for 10,000 entries, %v for 40", tc.gk.Kind, large, small)
}
}
}
// Opening a member of a large Application or Kustomization searches for the
// managers recording it; that costs the same however long the list naming it
// is, RBAC names Flux encodes included. Counting allocations can't tell:
// building a list sized up front is one allocation at any length.
func TestRecordingManagersCostDoesNotGrowWithTheManagersList(t *testing.T) {
rbac := func(name string) resourceid.Ref {
return resourceid.Ref{Group: "rbac.authorization.k8s.io", Kind: "RoleBinding", Namespace: "shop", Name: name}
}
service := func(name string) resourceid.Ref {
return resourceid.Ref{Kind: "Service", Namespace: "shop", Name: name}
}
for _, tc := range []struct {
manager resourceid.GroupKind
member func(string) resourceid.Ref
}{
{ownership.ArgoApplication, service},
{ownership.FluxKustomization, func(name string) resourceid.Ref { return rbac("metrics-server:" + name) }},
} {
t.Run(tc.manager.Kind, func(t *testing.T) {
target := tc.member("ui")
bytesPerSearch := func(n int) uint64 {
members := []resourceid.Ref{target}
for i := 1; i < n; i++ {
members = append(members, tc.member(fmt.Sprint("svc-", i)))
}
k, _ := ownershiptest.KindOf(tc.manager)
m := ownershiptest.Object{Ref: resourceid.Ref{Group: k.Group, Kind: k.Kind, Namespace: "ops", Name: "big"}, APIVersion: k.APIVersion(),
UID: "uid-big", ResourceVersion: "1", Membership: ownership.MembersRecorded, Members: members}
w := ownershiptest.World{Resources: ownershiptest.Resources(), Collection: map[resourceid.GroupKind]ownershiptest.Collection{}, Objects: []ownershiptest.Object{m}}
for _, item := range ownershiptest.Kinds {
w.Collection[item.GroupKind] = ownershiptest.Collection{OnDemand: !item.Typed}
}
s := begin(t, render(t, w).source())
if got := s.RecordingManagers(target); len(got.Managers) != 1 {
t.Fatalf("RecordingManagers(%v) = %+v", target, got)
}
const searches = 20
var before, after runtime.MemStats
runtime.ReadMemStats(&before)
for range searches {
s.RecordingManagers(target)
}
runtime.ReadMemStats(&after)
return (after.TotalAlloc - before.TotalAlloc) / searches
}
small, large := bytesPerSearch(40), bytesPerSearch(10000)
if large > small+64<<10 {
t.Fatalf("%d bytes per search when a 10,000-entry list names the object, %d for 40", large, small)
}
})
}
}
func TestRecordingManagersOfDynamicNativeAndForeignCollidingKinds(t *testing.T) {
members := []resourceid.Ref{
{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition", Name: "widgets.example.org"},
{Group: "apiregistration.k8s.io", Kind: "APIService", Name: "v1.metrics.example.org"},
{Group: "example.org", Kind: "CustomResourceDefinition", Namespace: "team", Name: "foreign-crd"},
{Group: "example.org", Kind: "APIService", Namespace: "team", Name: "foreign-api"},
}
app := ownershiptest.Object{
Ref: resourceid.Ref{Group: ownership.ArgoApplication.Group, Kind: ownership.ArgoApplication.Kind, Namespace: "argocd", Name: "native-apis"},
APIVersion: "argoproj.io/v1alpha1", UID: "uid-native-apis", ResourceVersion: "1",
Membership: ownership.MembersRecorded, Members: members, DestinationNamespace: "shop",
}
w := ownershiptest.World{Resources: ownershiptest.Resources(), Collection: map[resourceid.GroupKind]ownershiptest.Collection{}, Objects: []ownershiptest.Object{app}}
for _, k := range ownershiptest.Kinds {
w.Collection[k.GroupKind] = ownershiptest.Collection{OnDemand: !k.Typed}
}
for _, member := range members {
resource := "apiservices"
if member.Kind == "CustomResourceDefinition" {
resource = "customresourcedefinitions"
}
w.Resources = append(w.Resources, ownership.ServedResource{Group: member.Group, Version: "v1", Kind: member.Kind, Resource: resource, Namespaced: member.Namespace != "", Verbs: []string{"get", "list", "watch", "delete"}})
}
s := begin(t, render(t, w).source())
got := s.Members(app.Ref)
if got.Membership != ownership.MembersRecorded || len(got.Members) != len(members) {
t.Fatalf("Members = %+v", got)
}
for _, member := range members {
if !slices.Contains(got.Members, member) {
t.Fatalf("member placement changed: missing %v in %v", member, got.Members)
}
recorders := s.RecordingManagers(member)
if len(recorders.Managers) != 1 || recorders.Managers[0].Ref != app.Ref || !recorders.Complete() {
t.Fatalf("RecordingManagers(%v) = %+v, want %v alone", member, recorders, app.Ref)
}
}
}
func TestMembershipValidationTracksInventoryObservation(t *testing.T) {
s := &session{}
current := manager(ownership.ArgoApplication, localDestination, map[string]any{"resources": []any{}})
current.SetUID("first")
current.SetResourceVersion("1")
current.SetAPIVersion("argoproj.io/v1alpha1")
check := func(want ownership.MembershipState) {
t.Helper()
if got := s.cachedMemberState(ownership.ArgoApplication, current); got != want {
t.Fatalf("state = %v, want %v", got, want)
}
}
check(ownership.MembersRecorded)
current = current.DeepCopy()
current.SetResourceVersion("2")
delete(current.Object, "status")
check(ownership.MembersUnreadable)
current = current.DeepCopy()
current.SetResourceVersion("3")
current.Object["spec"] = map[string]any{"destination": map[string]any{"name": "remote"}}
check(ownership.MembersRemote)
current = current.DeepCopy()
current.SetUID("replacement")
current.Object["spec"] = localDestination
current.Object["status"] = map[string]any{"resources": []any{}}
check(ownership.MembersRecorded)
current = current.DeepCopy()
current.SetAPIVersion("argoproj.io/v1beta1")
delete(current.Object, "status")
check(ownership.MembersUnreadable)
current.SetResourceVersion("")
current.Object["status"] = map[string]any{"resources": []any{}}
check(ownership.MembersRecorded)
delete(current.Object, "status")
check(ownership.MembersUnreadable)
if len(s.membershipStates) != 1 {
t.Fatalf("retained %d observations", len(s.membershipStates))
}
}