pkg/ownership/resolve_test.go
Rama main · réplica de lectura del repositorio original.
package ownership_test
import (
"slices"
"strings"
"testing"
"k8s.io/apimachinery/pkg/api/meta"
apivalidation "k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/client-go/restmapper"
"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 object(t *testing.T, w ownershiptest.World, group, kind, namespace, name string) ownershiptest.Object {
t.Helper()
ref := resourceid.Ref{Group: group, Kind: kind, Namespace: namespace, Name: name}
for _, o := range w.Objects {
if o.Ref == ref {
return o
}
}
t.Fatalf("no %v in the World", ref)
return ownershiptest.Object{}
}
// resolve resolves the dependent's owner references in a scan of w.
func resolve(t *testing.T, w ownershiptest.World, dependent ownershiptest.Object) []ownership.OwnerResolution {
t.Helper()
s, _ := ownershiptest.Scan(w).Begin()
read := s.Object(dependent.Ref)
if read.State != ownership.Present {
t.Fatalf("%v = %v", dependent.Ref, read.State)
}
var out []ownership.OwnerResolution
for _, ref := range read.Object.OwnerReferences {
out = append(out, ownership.ResolveOwner(s, read.Object, ref))
}
return out
}
// A state's name goes on the wire; a value no constant names reads as
// unknown rather than panicking.
func TestOwnerStateNames(t *testing.T) {
for state, want := range map[ownership.OwnerState]string{
ownership.OwnerUnknown: "unknown", ownership.OwnerFound: "found", ownership.OwnerGone: "gone",
ownership.OwnerReplaced: "replaced", ownership.OwnerUnresolvable: "unresolvable", ownership.OwnerExcluded: "excluded",
ownership.OwnerWithheld: "withheld", ownership.OwnerState(99): "unknown",
} {
if got := state.String(); got != want {
t.Errorf("OwnerState(%d) = %q, want %q", state, got, want)
}
}
}
func TestMembershipStateNames(t *testing.T) {
for state, want := range map[ownership.MembershipState]string{
ownership.MembersUnreadable: "unreadable", ownership.MembersRecorded: "recorded", ownership.MembersRemote: "remote",
ownership.MembershipState(99): "unreadable",
} {
if got := state.String(); got != want {
t.Errorf("MembershipState(%d) = %q, want %q", state, got, want)
}
}
}
// Each case is how Kubernetes' garbage collector judges the reference.
func TestResolveOwnerJudgesReferencesAsTheCollectorDoes(t *testing.T) {
w := ownershiptest.Fixture()
ns := func(group, kind, namespace, name string) resourceid.Ref {
return resourceid.Ref{Group: group, Kind: kind, Namespace: namespace, Name: name}
}
for _, tc := range []struct {
name string
dependent ownershiptest.Object
want []ownership.OwnerState
wantRefs []resourceid.Ref
}{
{"controller chain", object(t, w, "", "Pod", "alpha", "web-1-a"), []ownership.OwnerState{ownership.OwnerFound}, []resourceid.Ref{ns("apps", "ReplicaSet", "alpha", "web-1")}},
{"two owners", object(t, w, "", "Pod", "alpha", "web-1-b"), []ownership.OwnerState{ownership.OwnerFound, ownership.OwnerFound}, []resourceid.Ref{ns("apps", "ReplicaSet", "alpha", "web-1"), ns("", "ConfigMap", "alpha", "cfg")}},
{"an owner in another namespace is looked up in the dependent's", object(t, w, "", "Pod", "beta", "stray"), []ownership.OwnerState{ownership.OwnerGone}, []resourceid.Ref{ns("apps", "ReplicaSet", "beta", "web-1")}},
{"the right UID under the wrong kind and name", object(t, w, "", "Pod", "alpha", "wrong-coordinates"), []ownership.OwnerState{ownership.OwnerGone}, []resourceid.Ref{ns("apps", "Deployment", "alpha", "nope")}},
{"a later incarnation", object(t, w, "", "Pod", "alpha", "replaced"), []ownership.OwnerState{ownership.OwnerReplaced}, []resourceid.Ref{ns("apps", "ReplicaSet", "alpha", "web-old")}},
{"no such owner", object(t, w, "", "Pod", "alpha", "dangling"), []ownership.OwnerState{ownership.OwnerGone}, []resourceid.Ref{ns("apps", "ReplicaSet", "alpha", "gone")}},
{"a cluster-scoped owner of a namespaced object", object(t, w, "example.io", "Gadget", "beta", "g1"), []ownership.OwnerState{ownership.OwnerFound}, []resourceid.Ref{ns("", "Node", "", "n1")}},
{"a cluster-scoped owner of a cluster-scoped object", object(t, w, "example.io", "ClusterWidget", "", "cw1"), []ownership.OwnerState{ownership.OwnerFound}, []resourceid.Ref{ns("", "Node", "", "n1")}},
{"a namespaced owner of a cluster-scoped object", object(t, w, "", "Node", "", "n2"), []ownership.OwnerState{ownership.OwnerUnresolvable}, []resourceid.Ref{{}}},
{"an owner version discovery doesn't list", object(t, w, "batch", "Job", "alpha", "j1"), []ownership.OwnerState{ownership.OwnerUnknown}, []resourceid.Ref{{}}},
{"an owner at another served version", object(t, w, "", "ConfigMap", "alpha", "versioned"), []ownership.OwnerState{ownership.OwnerFound}, []resourceid.Ref{ns("example.io", "Widget", "alpha", "w1")}},
} {
t.Run(tc.name, func(t *testing.T) {
got := resolve(t, w, tc.dependent)
if len(got) != len(tc.want) {
t.Fatalf("%d resolutions, want %d", len(got), len(tc.want))
}
for i := range got {
if got[i].State != tc.want[i] || got[i].Ref != tc.wantRefs[i] {
t.Errorf("reference %d = %v at %v, want %v at %v", i, got[i].State, got[i].Ref, tc.want[i], tc.wantRefs[i])
}
}
})
}
}
func TestResolveOwnerRefusesMalformedReferences(t *testing.T) {
w := ownershiptest.Fixture()
s, _ := ownershiptest.Scan(w).Begin()
pod := s.Object(resourceid.Ref{Kind: "Pod", Namespace: "alpha", Name: "web-1-a"}).Object
for _, ref := range []metav1.OwnerReference{
{APIVersion: "apps/v1/extra", Kind: "ReplicaSet", Name: "web-1", UID: "uid-alpha-web-1"},
{Kind: "ReplicaSet", Name: "web-1", UID: "uid-alpha-web-1"},
{APIVersion: "apps/v1", Name: "web-1", UID: "uid-alpha-web-1"},
{APIVersion: "apps/v1", Kind: "ReplicaSet", UID: "uid-alpha-web-1"},
} {
if got := ownership.ResolveOwner(s, pod, ref); got.State != ownership.OwnerUnresolvable {
t.Errorf("%+v = %v, want unresolvable", ref, got.State)
}
}
if got := ownership.ResolveOwner(s, pod, metav1.OwnerReference{APIVersion: "v1", Kind: "Event", Name: "e1", UID: "uid-alpha-e1"}); got.State != ownership.OwnerExcluded {
t.Errorf("an Event owner = %v, want excluded", got.State)
}
}
// A kind discovery doesn't list may be installed since: the collector retries
// it, so the owner is unknown, not unresolvable, even with discovery whole.
func TestResolveOwnerOfAKindDiscoveryDoesntListIsUnknown(t *testing.T) {
w := ownershiptest.Fixture()
s, _ := ownershiptest.Scan(w).Begin()
pod := s.Object(resourceid.Ref{Kind: "Pod", Namespace: "alpha", Name: "web-1-a"}).Object
got := ownership.ResolveOwner(s, pod, metav1.OwnerReference{APIVersion: "apps/v1", Kind: "Sprocket", Name: "x", UID: "uid-x"})
want := ownership.Gap{Kind: resourceid.GroupKind{Group: "apps", Kind: "Sprocket"}, Reason: readreason.KindUnknown}
if got.State != ownership.OwnerUnknown || got.Gap != want {
t.Fatalf("an owner kind discovery doesn't list = %v %+v, want unknown %+v", got.State, got.Gap, want)
}
}
// Without every owner's store, or without discovery's whole answer, the
// resolution is unknown, never gone or unresolvable.
func TestResolveOwnerIsUnknownWithoutTheEvidence(t *testing.T) {
w := ownershiptest.Fixture()
w.Collection[resourceid.GroupKind{Group: "apps", Kind: "ReplicaSet"}] = ownershiptest.Collection{State: ownershiptest.Loading}
got := resolve(t, w, object(t, w, "", "Pod", "alpha", "dangling"))
if got[0].State != ownership.OwnerUnknown || got[0].Gap.Reason != readreason.KindSyncPending {
t.Fatalf("owner in a loading kind = %v %+v, want unknown kind_sync_pending", got[0].State, got[0].Gap)
}
w = ownershiptest.Fixture()
w.Partial = true
if got := resolve(t, w, object(t, w, "batch", "Job", "alpha", "j1")); got[0].State != ownership.OwnerUnknown || got[0].Gap.Reason != readreason.ReadError {
t.Fatalf("unlisted owner version with partial discovery = %v %+v, want unknown read_error", got[0].State, got[0].Gap)
}
s, _ := ownershiptest.Scan(w).Begin()
pod := s.Object(resourceid.Ref{Kind: "Pod", Namespace: "alpha", Name: "web-1-a"}).Object
if got := ownership.ResolveOwner(s, pod, metav1.OwnerReference{APIVersion: "apps/v1", Kind: "Sprocket", Name: "x", UID: "uid-x"}); got.State != ownership.OwnerUnknown {
t.Fatalf("unlisted owner kind with partial discovery = %v, want unknown", got.State)
}
}
// An Event owner is excluded, as an Event read is, even when partial
// discovery didn't list Events.
func TestResolveOwnerExcludesEventOwnersWhateverDiscoveryLists(t *testing.T) {
w := ownershiptest.Fixture()
w.Partial = true
var resources []ownership.ServedResource
for _, r := range w.Resources {
if r.Kind != "Event" {
resources = append(resources, r)
}
}
w.Resources = resources
s, _ := ownershiptest.Scan(w).Begin()
pod := s.Object(resourceid.Ref{Kind: "Pod", Namespace: "alpha", Name: "web-1-a"}).Object
for _, apiVersion := range []string{"v1", "events.k8s.io/v1"} {
if got := ownership.ResolveOwner(s, pod, metav1.OwnerReference{APIVersion: apiVersion, Kind: "Event", Name: "e1", UID: "uid-alpha-e1"}); got.State != ownership.OwnerExcluded {
t.Errorf("an Event owner at %s, Events unlisted = %v %+v, want excluded", apiVersion, got.State, got.Gap)
}
}
}
// collectorMapper is the discovery RESTMapper the garbage collector resolves
// owner references through, built from the same served resources.
func collectorMapper(resources []ownership.ServedResource) meta.RESTMapper {
byGroup := map[string]*restmapper.APIGroupResources{}
var groups []*restmapper.APIGroupResources
for _, r := range resources {
g := byGroup[r.Group]
if g == nil {
g = &restmapper.APIGroupResources{Group: metav1.APIGroup{Name: r.Group}, VersionedResources: map[string][]metav1.APIResource{}}
byGroup[r.Group] = g
groups = append(groups, g)
}
gv := schema.GroupVersion{Group: r.Group, Version: r.Version}.String()
if !slices.ContainsFunc(g.Group.Versions, func(v metav1.GroupVersionForDiscovery) bool { return v.Version == r.Version }) {
g.Group.Versions = append(g.Group.Versions, metav1.GroupVersionForDiscovery{GroupVersion: gv, Version: r.Version})
}
g.VersionedResources[r.Version] = append(g.VersionedResources[r.Version], metav1.APIResource{Name: r.Resource, Kind: r.Kind, Namespaced: r.Namespaced, Verbs: r.Verbs})
}
for _, g := range groups {
g.Group.PreferredVersion = g.Group.Versions[0]
}
return restmapper.NewDiscoveryRESTMapper(groups)
}
// Every spelling of an owner reference the API server admits must resolve as
// the collector maps it, or be unknown where Radar can't tell: where its
// RESTMapper maps nothing, the collector retries, so the owner is unknown;
// where it maps a namespaced owner for a cluster-scoped object, the reference
// is unresolvable; where it maps a resource no API serves, as for the List
// kinds it makes up, Radar leaves the owner unknown; otherwise the owner is
// the object of the mapped kind at the reference's name, judged by UID,
// unless Radar can't read that kind.
func TestResolveOwnerMapsReferencesAsTheCollectorsRESTMapperDoes(t *testing.T) {
w := ownershiptest.Fixture()
s, _ := ownershiptest.Scan(w).Begin()
mapper := collectorMapper(w.Resources)
kindOf := map[schema.GroupResource]resourceid.GroupKind{}
for _, r := range w.Resources {
kindOf[schema.GroupResource{Group: r.Group, Resource: r.Resource}] = resourceid.GroupKind{Group: r.Group, Kind: r.Kind}
}
dependents := []ownership.Observation{
{Ref: resourceid.Ref{Kind: "Pod", Namespace: "alpha", Name: "probe"}},
{Ref: resourceid.Ref{Kind: "Node", Name: "probe"}},
}
checked := 0
for _, r := range w.Resources {
if ownership.IsEvent(resourceid.GroupKind{Group: r.Group, Kind: r.Kind}) {
continue
}
var names []string
uids := map[string]types.UID{}
for _, o := range w.Objects {
if o.Ref.Group == r.Group && o.Ref.Kind == r.Kind && !slices.Contains(names, o.Ref.Name) {
names = append(names, o.Ref.Name)
uids[o.Ref.Name] = o.UID
}
}
names = append(names, "missing")
spellings := []string{r.Kind, strings.ToLower(r.Kind), strings.ToUpper(r.Kind), r.Kind[:1] + strings.ToLower(r.Kind[1:]), r.Kind + "List", strings.ToLower(r.Kind) + "List", "List"}
for _, version := range []string{r.Version, "v9", runtime.APIVersionInternal} {
for _, kind := range spellings {
for _, name := range names {
for _, uid := range []types.UID{uids[name], "uid-other"} {
if uid == "" {
continue
}
ref := metav1.OwnerReference{APIVersion: resourceid.APIVersion(r.Group, version), Kind: kind, Name: name, UID: uid}
if errs := apivalidation.ValidateOwnerReferences([]metav1.OwnerReference{ref}, field.NewPath("ownerReferences")); len(errs) > 0 {
continue
}
for _, dependent := range dependents {
checked++
got := ownership.ResolveOwner(s, dependent, ref)
want, wantRef, readable := collectorResolves(t, w, mapper, kindOf, dependent, ref)
if got.State == ownership.OwnerUnknown && readable == false {
continue
}
if got.State != want || (want != ownership.OwnerUnresolvable && got.Ref != wantRef) {
t.Errorf("%s %s/%s uid %s, dependent %v: ResolveOwner = %v at %v; the collector maps it to %v at %v", ref.APIVersion, ref.Kind, ref.Name, ref.UID, dependent.Ref, got.State, got.Ref, want, wantRef)
}
}
}
}
}
}
}
if checked < 500 {
t.Fatalf("only %d references checked", checked)
}
}
// collectorResolves is what the collector's RESTMapper and a GET make of ref.
// readable is false when Radar can't read the mapped kind, so an unknown
// answer is honest. Radar also leaves unknown the List kinds the RESTMapper
// makes up, whose resources no API serves.
func collectorResolves(t *testing.T, w ownershiptest.World, mapper meta.RESTMapper, kindOf map[schema.GroupResource]resourceid.GroupKind, dependent ownership.Observation, ref metav1.OwnerReference) (ownership.OwnerState, resourceid.Ref, bool) {
t.Helper()
gv, err := schema.ParseGroupVersion(ref.APIVersion)
if err != nil {
return ownership.OwnerUnresolvable, resourceid.Ref{}, true
}
mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gv.Group, Kind: ref.Kind}, gv.Version)
if err != nil {
// The collector retries a mapping error: the next discovery may serve
// the kind.
return ownership.OwnerUnknown, resourceid.Ref{}, true
}
gk, served := kindOf[mapping.Resource.GroupResource()]
if !served {
return ownership.OwnerGone, resourceid.Ref{}, false
}
namespaced := mapping.Scope.Name() == meta.RESTScopeNameNamespace
if namespaced && dependent.Ref.Namespace == "" {
return ownership.OwnerUnresolvable, resourceid.Ref{}, true
}
at := resourceid.Ref{Group: gv.Group, Kind: gk.Kind, Name: ref.Name}
if namespaced {
at.Namespace = dependent.Ref.Namespace
}
if c := w.Collection[gk]; c.State != ownershiptest.Collected {
return ownership.OwnerUnknown, at, false
}
for _, o := range w.Objects {
if o.Ref == at {
if o.UID == ref.UID {
return ownership.OwnerFound, at, true
}
return ownership.OwnerReplaced, at, true
}
}
return ownership.OwnerGone, at, true
}
// References the collector's RESTMapper settles by the order it registered
// resources in, which Radar can't know, stay unknown rather than resolving to
// one candidate: a kind in lower case that two kinds differing only in case
// share, a served kind that is another's lower-case spelling, and the List
// kinds it makes up, one of which can name a served resource.
func TestResolveOwnerLeavesOrderDependentMappingsUnknown(t *testing.T) {
gc := []string{"create", "delete", "get", "list", "patch", "update", "watch"}
w := ownershiptest.Fixture()
w.Resources = append(w.Resources,
ownership.ServedResource{Group: "shapes.example.io", Version: "v1", Kind: "Thing", Resource: "things", Namespaced: true, Verbs: gc},
ownership.ServedResource{Group: "shapes.example.io", Version: "v1", Kind: "THING", Resource: "allcapsthings", Namespaced: true, Verbs: gc},
ownership.ServedResource{Group: "shapes.example.io", Version: "v1", Kind: "Gizmo", Resource: "gizmos", Namespaced: true, Verbs: gc},
ownership.ServedResource{Group: "shapes.example.io", Version: "v1", Kind: "Gizmo", Resource: "gizmoalts", Namespaced: true, Verbs: gc},
ownership.ServedResource{Group: "shapes.example.io", Version: "v1", Kind: "dial", Resource: "lowerdials", Namespaced: true, Verbs: gc},
ownership.ServedResource{Group: "shapes.example.io", Version: "v1", Kind: "Dial", Resource: "dials", Namespaced: true, Verbs: gc},
ownership.ServedResource{Group: "shapes.example.io", Version: "v1", Kind: "Other", Resource: "diallists", Namespaced: true, Verbs: gc},
)
s, err := ownershiptest.Scan(w).Begin()
if err != nil {
t.Fatal(err)
}
pod := ownership.Observation{Ref: resourceid.Ref{Kind: "Pod", Namespace: "alpha", Name: "probe"}}
for _, kind := range []string{"thing", "dial", "GizmoList", "DialList", "List"} {
got := ownership.ResolveOwner(s, pod, metav1.OwnerReference{APIVersion: "shapes.example.io/v1", Kind: kind, Name: "x", UID: "uid-x"})
if got.State != ownership.OwnerUnknown || got.Gap.Reason == "" {
t.Errorf("%s: %v %+v, want unknown with a reason", kind, got.State, got.Gap)
}
}
}