package discovery import ( "errors" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientdiscovery "k8s.io/client-go/discovery" "github.com/sophotechlabs/spinoza/internal/api" ) type stubDiscovery struct { clientdiscovery.DiscoveryInterface lists []*metav1.APIResourceList err error } func (s *stubDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) { return s.lists, s.err } func listWatch() metav1.Verbs { return metav1.Verbs{"get", "list", "watch"} } func coreList() *metav1.APIResourceList { return &metav1.APIResourceList{ GroupVersion: "v1", APIResources: []metav1.APIResource{ {Name: "pods", Kind: "Pod", Namespaced: true, Verbs: listWatch()}, {Name: "pods/log", Kind: "Pod", Namespaced: true, Verbs: metav1.Verbs{"get"}}, {Name: "services", Kind: "Service", Namespaced: true, Verbs: listWatch()}, {Name: "nodes", Kind: "Node", Namespaced: false, Verbs: listWatch()}, {Name: "configmaps", Kind: "ConfigMap", Namespaced: true, Verbs: listWatch()}, {Name: "persistentvolumes", Kind: "PersistentVolume", Namespaced: false, Verbs: listWatch()}, {Name: "serviceaccounts", Kind: "ServiceAccount", Namespaced: true, Verbs: listWatch()}, {Name: "componentstatuses", Kind: "ComponentStatus", Namespaced: false, Verbs: listWatch()}, {Name: "bindings", Kind: "Binding", Namespaced: true, Verbs: metav1.Verbs{"create"}}, }, } } func appsList() *metav1.APIResourceList { return &metav1.APIResourceList{ GroupVersion: "apps/v1", APIResources: []metav1.APIResource{ {Name: "deployments", Kind: "Deployment", Namespaced: true, Verbs: listWatch()}, {Name: "daemonsets", Kind: "DaemonSet", Namespaced: true, Verbs: listWatch()}, }, } } func crdList() *metav1.APIResourceList { return &metav1.APIResourceList{ GroupVersion: "example.com/v1alpha1", APIResources: []metav1.APIResource{ {Name: "widgets", Kind: "Widget", Namespaced: true, Verbs: listWatch()}, }, } } func fullLists() []*metav1.APIResourceList { return []*metav1.APIResourceList{coreList(), appsList(), crdList()} } func catByName(cats []api.Category, name string) (api.Category, bool) { for _, c := range cats { if c.Name == name { return c, true } } return api.Category{}, false } func hasResource(cat api.Category, resource string) bool { for _, r := range cat.Resources { if r.Resource == resource { return true } } return false } func TestKey(t *testing.T) { result := Key("apps", "v1", "deployments") expected := "apps/v1/deployments" if result != expected { t.Fatalf("Key = %q, want %q", result, expected) } } func TestKeyCoreGroup(t *testing.T) { result := Key("", "v1", "pods") expected := "/v1/pods" if result != expected { t.Fatalf("Key = %q, want %q", result, expected) } } func TestSupportsListWatch(t *testing.T) { cases := []struct { name string verbs metav1.Verbs want bool }{ {"list and watch", metav1.Verbs{"get", "list", "watch"}, true}, {"list only", metav1.Verbs{"get", "list"}, false}, {"watch only", metav1.Verbs{"watch"}, false}, {"neither", metav1.Verbs{"get", "create"}, false}, {"empty", metav1.Verbs{}, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { result := supportsListWatch(tc.verbs) if result != tc.want { t.Fatalf("supportsListWatch(%v) = %v, want %v", tc.verbs, result, tc.want) } }) } } func TestCategoryFor(t *testing.T) { cases := []struct { group string resource string want string }{ {"apps", "deployments", "Workloads"}, {"batch", "jobs", "Workloads"}, {"networking.k8s.io", "ingresses", "Network"}, {"discovery.k8s.io", "endpointslices", "Network"}, {"storage.k8s.io", "storageclasses", "Storage"}, {"rbac.authorization.k8s.io", "roles", "Access Control"}, {"autoscaling", "horizontalpodautoscalers", "Config"}, {"policy", "poddisruptionbudgets", "Config"}, {"scheduling.k8s.io", "priorityclasses", "Config"}, {"node.k8s.io", "runtimeclasses", "Config"}, {"coordination.k8s.io", "leases", "Config"}, {"apiextensions.k8s.io", "customresourcedefinitions", "Cluster"}, {"apiregistration.k8s.io", "apiservices", "Cluster"}, {"admissionregistration.k8s.io", "validatingwebhookconfigurations", "Cluster"}, {"certificates.k8s.io", "certificatesigningrequests", "Cluster"}, {"authentication.k8s.io", "tokenreviews", "Cluster"}, {"authorization.k8s.io", "subjectaccessreviews", "Cluster"}, {"flowcontrol.apiserver.k8s.io", "flowschemas", "Cluster"}, {"events.k8s.io", "events", "Cluster"}, {"example.com", "widgets", "Custom Resources"}, {"cert-manager.io", "certificates", "Custom Resources"}, } for _, tc := range cases { t.Run(tc.group+"/"+tc.resource, func(t *testing.T) { result := categoryFor(tc.group, tc.resource) if result != tc.want { t.Fatalf("categoryFor(%q, %q) = %q, want %q", tc.group, tc.resource, result, tc.want) } }) } } func TestCoreCategoryFor(t *testing.T) { cases := []struct { resource string want string }{ {"pods", "Workloads"}, {"replicationcontrollers", "Workloads"}, {"configmaps", "Config"}, {"secrets", "Config"}, {"resourcequotas", "Config"}, {"limitranges", "Config"}, {"services", "Network"}, {"endpoints", "Network"}, {"persistentvolumeclaims", "Storage"}, {"persistentvolumes", "Storage"}, {"serviceaccounts", "Access Control"}, {"namespaces", "Cluster"}, {"nodes", "Cluster"}, {"componentstatuses", "Cluster"}, } for _, tc := range cases { t.Run(tc.resource, func(t *testing.T) { result := coreCategoryFor(tc.resource) if result != tc.want { t.Fatalf("coreCategoryFor(%q) = %q, want %q", tc.resource, result, tc.want) } }) } } func TestListCategorizes(t *testing.T) { stub := &stubDiscovery{lists: fullLists()} cats, byKey, err := List(stub) if err != nil { t.Fatalf("List: %v", err) } workloads, ok := catByName(cats, "Workloads") if !ok { t.Fatal("missing Workloads category") } if !hasResource(workloads, "pods") { t.Fatal("Workloads missing pods") } if !hasResource(workloads, "deployments") { t.Fatal("Workloads missing deployments") } if !hasResource(workloads, "daemonsets") { t.Fatal("Workloads missing daemonsets") } network, ok := catByName(cats, "Network") if !ok { t.Fatal("missing Network category") } if !hasResource(network, "services") { t.Fatal("Network missing services") } config, ok := catByName(cats, "Config") if !ok { t.Fatal("missing Config category") } if !hasResource(config, "configmaps") { t.Fatal("Config missing configmaps") } storage, ok := catByName(cats, "Storage") if !ok { t.Fatal("missing Storage category") } if !hasResource(storage, "persistentvolumes") { t.Fatal("Storage missing persistentvolumes") } access, ok := catByName(cats, "Access Control") if !ok { t.Fatal("missing Access Control category") } if !hasResource(access, "serviceaccounts") { t.Fatal("Access Control missing serviceaccounts") } cluster, ok := catByName(cats, "Cluster") if !ok { t.Fatal("missing Cluster category") } if !hasResource(cluster, "nodes") { t.Fatal("Cluster missing nodes") } if !hasResource(cluster, "componentstatuses") { t.Fatal("Cluster missing componentstatuses") } custom, ok := catByName(cats, "Custom Resources") if !ok { t.Fatal("missing Custom Resources category") } if !hasResource(custom, "widgets") { t.Fatal("Custom Resources missing widgets") } desc, ok := byKey[Key("apps", "v1", "deployments")] if !ok { t.Fatal("byKey missing apps/v1/deployments") } if desc.Kind != "Deployment" { t.Fatalf("Kind = %q, want Deployment", desc.Kind) } if desc.Category != "Workloads" { t.Fatalf("Category = %q, want Workloads", desc.Category) } if !desc.Namespaced { t.Fatal("deployments Namespaced = false, want true") } } func TestListSkipsSubresources(t *testing.T) { stub := &stubDiscovery{lists: fullLists()} _, byKey, err := List(stub) if err != nil { t.Fatalf("List: %v", err) } if _, ok := byKey[Key("", "v1", "pods/log")]; ok { t.Fatal("byKey contains subresource pods/log, want skipped") } } func TestListSkipsNonListWatch(t *testing.T) { stub := &stubDiscovery{lists: fullLists()} _, byKey, err := List(stub) if err != nil { t.Fatalf("List: %v", err) } if _, ok := byKey[Key("", "v1", "bindings")]; ok { t.Fatal("byKey contains bindings (create-only), want skipped") } } func TestListOrdersCategories(t *testing.T) { stub := &stubDiscovery{lists: fullLists()} cats, _, err := List(stub) if err != nil { t.Fatalf("List: %v", err) } names := make([]string, 0, len(cats)) for _, c := range cats { names = append(names, c.Name) } expected := []string{"Cluster", "Workloads", "Config", "Network", "Storage", "Access Control", "Custom Resources"} if len(names) != len(expected) { t.Fatalf("categories = %v, want %v", names, expected) } for i := range expected { if names[i] != expected[i] { t.Fatalf("category order = %v, want %v", names, expected) } } } func TestListOrdersWorkloadsByEverydayUse(t *testing.T) { stub := &stubDiscovery{lists: fullLists()} cats, _, err := List(stub) if err != nil { t.Fatalf("List: %v", err) } workloads, ok := catByName(cats, "Workloads") if !ok { t.Fatal("missing Workloads category") } resources := make([]string, 0, len(workloads.Resources)) for _, r := range workloads.Resources { resources = append(resources, r.Resource) } expected := []string{"pods", "deployments", "daemonsets"} for i := range expected { if resources[i] != expected[i] { t.Fatalf("Workloads resources = %v, want %v first", resources, expected) } } } func TestListPutsUnrankedKindsAfterRankedOnes(t *testing.T) { lists := []*metav1.APIResourceList{ { GroupVersion: "apps/v1", APIResources: []metav1.APIResource{ {Name: "controllerrevisions", Kind: "ControllerRevision", Namespaced: true, Verbs: listWatch()}, {Name: "deployments", Kind: "Deployment", Namespaced: true, Verbs: listWatch()}, }, }, { GroupVersion: "v1", APIResources: []metav1.APIResource{ {Name: "pods", Kind: "Pod", Namespaced: true, Verbs: listWatch()}, }, }, } cats, _, err := List(&stubDiscovery{lists: lists}) if err != nil { t.Fatalf("List: %v", err) } workloads, ok := catByName(cats, "Workloads") if !ok { t.Fatal("missing Workloads category") } kinds := make([]string, 0, len(workloads.Resources)) for _, r := range workloads.Resources { kinds = append(kinds, r.Kind) } want := []string{"Pod", "Deployment", "ControllerRevision"} for i := range want { if kinds[i] != want[i] { t.Fatalf("kinds = %v, want %v", kinds, want) } } } func TestListSortsUnrankedCategoriesAlphabetically(t *testing.T) { lists := []*metav1.APIResourceList{ { GroupVersion: "cilium.io/v2", APIResources: []metav1.APIResource{ {Name: "ciliumnetworkpolicies", Kind: "CiliumNetworkPolicy", Namespaced: true, Verbs: listWatch()}, {Name: "ciliumendpoints", Kind: "CiliumEndpoint", Namespaced: true, Verbs: listWatch()}, }, }, } cats, _, err := List(&stubDiscovery{lists: lists}) if err != nil { t.Fatalf("List: %v", err) } custom, ok := catByName(cats, "Custom Resources") if !ok { t.Fatal("missing Custom Resources category") } if custom.Resources[0].Resource != "ciliumendpoints" { t.Fatalf("resources = %v, want alphabetical when unranked", custom.Resources) } } func TestListOrdersExtraCategoriesAlphabetically(t *testing.T) { lists := []*metav1.APIResourceList{ { GroupVersion: "zebra.io/v1", APIResources: []metav1.APIResource{{Name: "zebras", Kind: "Zebra", Namespaced: true, Verbs: listWatch()}}, }, { GroupVersion: "alpha.io/v1", APIResources: []metav1.APIResource{{Name: "alphas", Kind: "Alpha", Namespaced: true, Verbs: listWatch()}}, }, } stub := &stubDiscovery{lists: lists} cats, _, err := List(stub) if err != nil { t.Fatalf("List: %v", err) } if len(cats) != 1 { t.Fatalf("categories = %d, want 1 (Custom Resources)", len(cats)) } custom := cats[0] if custom.Name != "Custom Resources" { t.Fatalf("category = %q, want Custom Resources", custom.Name) } if len(custom.Resources) != 2 { t.Fatalf("resources = %d, want 2", len(custom.Resources)) } if custom.Resources[0].Resource != "alphas" { t.Fatalf("first resource = %q, want alphas", custom.Resources[0].Resource) } } func TestListSkipsNilAndUnparseableLists(t *testing.T) { lists := []*metav1.APIResourceList{ nil, { GroupVersion: "bad/group/version", APIResources: []metav1.APIResource{{Name: "broken", Kind: "Broken", Verbs: listWatch()}}, }, coreList(), } stub := &stubDiscovery{lists: lists} cats, byKey, err := List(stub) if err != nil { t.Fatalf("List: %v", err) } if _, ok := byKey[Key("", "v1", "pods")]; !ok { t.Fatal("byKey missing pods after nil/unparseable lists") } if _, ok := catByName(cats, "Workloads"); !ok { t.Fatal("missing Workloads category") } } func TestGroupByCategoryOrdersExtraAlphabetically(t *testing.T) { descs := []api.ResourceDescriptor{ {Resource: "deployments", Category: "Workloads"}, {Resource: "zebras", Category: "Zebra"}, {Resource: "alphas", Category: "Alpha"}, } cats := groupByCategory(descs) names := make([]string, 0, len(cats)) for _, c := range cats { names = append(names, c.Name) } expected := []string{"Workloads", "Alpha", "Zebra"} if len(names) != len(expected) { t.Fatalf("categories = %v, want %v", names, expected) } for i := range expected { if names[i] != expected[i] { t.Fatalf("category order = %v, want %v", names, expected) } } } func TestListPropagatesError(t *testing.T) { stub := &stubDiscovery{lists: fullLists(), err: errors.New("partial discovery")} _, byKey, err := List(stub) if err == nil { t.Fatal("List returned nil error, want propagated error") } if _, ok := byKey[Key("", "v1", "pods")]; !ok { t.Fatal("byKey missing pods on partial error") } } func TestTheEventsAliasIsLeftOutWhenBothApisAnswer(t *testing.T) { client := &stubDiscovery{lists: []*metav1.APIResourceList{ { GroupVersion: "v1", APIResources: []metav1.APIResource{ {Name: "events", Kind: "Event", Namespaced: true, Verbs: listWatch()}, }, }, { GroupVersion: "events.k8s.io/v1", APIResources: []metav1.APIResource{ {Name: "events", Kind: "Event", Namespaced: true, Verbs: listWatch()}, }, }, }} categories, byKey, err := List(client) if err != nil { t.Fatalf("list: %v", err) } kinds := []string{} for _, category := range categories { for _, desc := range category.Resources { kinds = append(kinds, desc.Group+"/"+desc.Resource) } } if len(kinds) != 1 || kinds[0] != "/events" { t.Fatalf("kinds = %v, want the core events only", kinds) } if _, ok := byKey[Key("events.k8s.io", "v1", "events")]; !ok { t.Fatal("the alias should still be reachable by key for anything that asks for it") } } func TestTheEventsAliasStandsInWhenCoreEventsAreMissing(t *testing.T) { client := &stubDiscovery{lists: []*metav1.APIResourceList{ { GroupVersion: "events.k8s.io/v1", APIResources: []metav1.APIResource{ {Name: "events", Kind: "Event", Namespaced: true, Verbs: listWatch()}, }, }, }} categories, _, err := List(client) if err != nil { t.Fatalf("list: %v", err) } if len(categories) != 1 || len(categories[0].Resources) != 1 { t.Fatalf("categories = %+v, want the events.k8s.io kind kept", categories) } }