internal/discovery/discovery_test.go
Rama main · réplica de lectura del repositorio original.
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)
}
}