Ctrl K
ComunidadGuías…

pkg/k8score/discovery_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package k8score

import (
	"errors"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/discovery"
	fakediscovery "k8s.io/client-go/discovery/fake"
	fakeclientset "k8s.io/client-go/kubernetes/fake"
)

type countingDiscovery struct {
	discovery.DiscoveryInterface
	calls atomic.Int32
}

func TestIsBuiltInAPIGroupKeepsDynamicBuiltInsDistinctFromCRDs(t *testing.T) {
	for _, group := range []string{"apps", "resource.k8s.io", "authentication.k8s.io", "authorization.k8s.io", "apiregistration.k8s.io"} {
		if !IsBuiltInAPIGroup(group) {
			t.Errorf("IsBuiltInAPIGroup(%q) = false", group)
		}
	}
	if IsBuiltInAPIGroup("batch.volcano.sh") {
		t.Fatal("a CRD API group was classified as built in")
	}

	fakeDiscovery := fakeclientset.NewSimpleClientset().Discovery().(*fakediscovery.FakeDiscovery)
	fakeDiscovery.Resources = []*metav1.APIResourceList{{
		GroupVersion: "resource.k8s.io/v1",
		APIResources: []metav1.APIResource{{Name: "resourceclaims", Kind: "ResourceClaim", Verbs: []string{"get", "list", "watch"}}},
	}}
	d, err := NewResourceDiscovery(fakeDiscovery)
	if err != nil {
		t.Fatalf("NewResourceDiscovery: %v", err)
	}
	resource, ok := d.GetResourceWithGroup("ResourceClaim", "resource.k8s.io")
	if !ok || !resource.IsCRD {
		t.Fatalf("DRA must remain dynamically watched: resource=%+v ok=%v", resource, ok)
	}
}

func (d *countingDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
	d.calls.Add(1)
	return d.DiscoveryInterface.ServerGroupsAndResources()
}

func TestRefreshIfStaleCoalescesConcurrentRefreshes(t *testing.T) {
	fakeDiscovery := fakeclientset.NewSimpleClientset().Discovery().(*fakediscovery.FakeDiscovery)
	fakeDiscovery.Resources = []*metav1.APIResourceList{{
		GroupVersion: "metrics.k8s.io/v1",
		APIResources: []metav1.APIResource{{Name: "pods", Kind: "PodMetrics"}},
	}}
	client := &countingDiscovery{DiscoveryInterface: fakeDiscovery}
	d, err := NewResourceDiscovery(client, WithDiscoveryCacheTTL(time.Hour))
	if err != nil {
		t.Fatalf("NewResourceDiscovery: %v", err)
	}
	client.calls.Store(0)
	d.mu.Lock()
	d.lastRefresh = time.Now().Add(-2 * time.Hour)
	d.mu.Unlock()

	const callers = 20
	start := make(chan struct{})
	errs := make(chan error, callers)
	var wg sync.WaitGroup
	for range callers {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			errs <- d.RefreshIfStale()
		}()
	}
	close(start)
	wg.Wait()
	close(errs)
	for err := range errs {
		if err != nil {
			t.Fatalf("RefreshIfStale: %v", err)
		}
	}
	if got := client.calls.Load(); got != 1 {
		t.Fatalf("ServerGroupsAndResources calls = %d, want 1", got)
	}
}

func TestRefreshPreservesSnapshotOnEmptyDiscoveryError(t *testing.T) {
	initial := []*metav1.APIResourceList{{
		GroupVersion: "metrics.k8s.io/v1",
		APIResources: []metav1.APIResource{{Name: "pods", Kind: "PodMetrics"}},
	}}
	client := &snapshotDiscovery{resources: initial}
	d, err := NewResourceDiscovery(client)
	if err != nil {
		t.Fatalf("NewResourceDiscovery: %v", err)
	}

	client.resources = nil
	client.err = errors.New("temporary discovery failure")
	beforeRefresh := d.Stats().LastRefresh
	if err := d.Refresh(); err == nil {
		t.Fatal("Refresh unexpectedly succeeded")
	}
	gvr, ok := d.GetGVRWithGroup("pods", MetricsAPIGroup)
	if !ok || gvr.Version != "v1" {
		t.Fatalf("preserved metrics GVR = %v, ok=%v, want metrics.k8s.io/v1", gvr, ok)
	}
	if !d.Stats().LastRefresh.After(beforeRefresh) {
		t.Fatal("failed refresh did not advance the retry cooldown")
	}
}

type snapshotDiscovery struct {
	discovery.DiscoveryInterface
	resources []*metav1.APIResourceList
	err       error
}

func (d *snapshotDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
	return nil, d.resources, d.err
}

func TestAddAPIResourceRegistersGroupQualifiedCRD(t *testing.T) {
	d := &ResourceDiscovery{
		resourceMap: make(map[string]APIResource),
		gvrMap:      make(map[string]schema.GroupVersionResource),
	}

	d.AddAPIResource(APIResource{
		Group:      "networking.istio.io",
		Version:    "v1",
		Kind:       "VirtualService",
		Name:       "virtualservices",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get", "list", "watch"},
	})

	gvr, ok := d.GetGVRWithGroup("VirtualService", "networking.istio.io")
	if !ok {
		t.Fatal("expected group-qualified VirtualService lookup to resolve")
	}
	if gvr != (schema.GroupVersionResource{Group: "networking.istio.io", Version: "v1", Resource: "virtualservices"}) {
		t.Fatalf("GVR = %v, want networking.istio.io/v1 virtualservices", gvr)
	}
	if !d.SupportsWatchGVR(gvr) {
		t.Fatal("expected exact fallback GVR to support watch")
	}
	if got := d.GetKindForGVR(gvr); got != "VirtualService" {
		t.Fatalf("kind = %q, want VirtualService", got)
	}
}

func TestGetGVRWithGroupPrefersMostStableVersion(t *testing.T) {
	d := &ResourceDiscovery{
		resourceMap: make(map[string]APIResource),
		gvrMap:      make(map[string]schema.GroupVersionResource),
	}

	// Both versions served; deprecated v1beta1 discovered first. Informers are
	// registered against the storage version (v1), so the count lookup must
	// resolve to v1 — not the first-discovered v1beta1.
	d.AddAPIResource(APIResource{
		Group:      "gateway.networking.k8s.io",
		Version:    "v1beta1",
		Kind:       "Gateway",
		Name:       "gateways",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get", "list", "watch"},
	})
	d.AddAPIResource(APIResource{
		Group:      "gateway.networking.k8s.io",
		Version:    "v1",
		Kind:       "Gateway",
		Name:       "gateways",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get", "list", "watch"},
	})

	gvr, ok := d.GetGVRWithGroup("Gateway", "gateway.networking.k8s.io")
	if !ok {
		t.Fatal("expected group-qualified Gateway lookup to resolve")
	}
	if gvr.Version != "v1" {
		t.Fatalf("version = %q, want v1 (storage version, not first-discovered v1beta1)", gvr.Version)
	}
}

func TestSchedulingPodGroupPrefersBetaAndStaysGroupQualified(t *testing.T) {
	d := &ResourceDiscovery{
		resourceMap: make(map[string]APIResource),
		gvrMap:      make(map[string]schema.GroupVersionResource),
	}
	for _, resource := range []APIResource{
		{Group: "scheduling.k8s.io", Version: "v1alpha3", Kind: "PodGroup", Name: "podgroups", Namespaced: true, Verbs: []string{"get", "list", "watch"}},
		{Group: "scheduling.k8s.io", Version: "v1beta1", Kind: "PodGroup", Name: "podgroups", Namespaced: true, Verbs: []string{"get", "list", "watch"}},
		{Group: "example.io", Version: "v1", Kind: "PodGroup", Name: "custompodgroups", Namespaced: true, IsCRD: true, Verbs: []string{"get", "list", "watch"}},
		{Group: "scheduling.k8s.io", Version: "v1alpha3", Kind: "CompositePodGroup", Name: "compositepodgroups", Namespaced: true, Verbs: []string{"get", "list", "watch"}},
	} {
		d.AddAPIResource(resource)
	}

	podGroup, ok := d.GetGVRWithGroup("PodGroup", "scheduling.k8s.io")
	if !ok || podGroup != (schema.GroupVersionResource{Group: "scheduling.k8s.io", Version: "v1beta1", Resource: "podgroups"}) {
		t.Fatalf("scheduling PodGroup GVR = %v, ok=%v", podGroup, ok)
	}
	custom, ok := d.GetGVRWithGroup("PodGroup", "example.io")
	if !ok || custom.Resource != "custompodgroups" {
		t.Fatalf("custom PodGroup GVR = %v, ok=%v", custom, ok)
	}
	composite, ok := d.GetGVRWithGroup("CompositePodGroup", "scheduling.k8s.io")
	if !ok || composite.Version != "v1alpha3" {
		t.Fatalf("CompositePodGroup GVR = %v, ok=%v", composite, ok)
	}
}

func TestSupportsWatchGVRUsesExactGroupVersionResource(t *testing.T) {
	d := &ResourceDiscovery{
		resourceMap: make(map[string]APIResource),
		gvrMap:      make(map[string]schema.GroupVersionResource),
	}

	d.AddAPIResource(APIResource{
		Group:      "gateway.networking.k8s.io",
		Version:    "v1",
		Kind:       "Gateway",
		Name:       "gateways",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get", "list", "watch"},
	})
	d.AddAPIResource(APIResource{
		Group:      "networking.istio.io",
		Version:    "v1",
		Kind:       "Gateway",
		Name:       "gateways",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get", "list"},
	})

	gatewayAPI := schema.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1", Resource: "gateways"}
	istio := schema.GroupVersionResource{Group: "networking.istio.io", Version: "v1", Resource: "gateways"}
	if !d.SupportsWatchGVR(gatewayAPI) {
		t.Fatal("Gateway API GVR should support watch")
	}
	if d.SupportsWatchGVR(istio) {
		t.Fatal("Istio Gateway GVR should not inherit watch support from Gateway API Gateway")
	}
	if got := d.GetKindForGVR(istio); got != "Gateway" {
		t.Fatalf("kind = %q, want Gateway", got)
	}
}

func TestAddAPIResourceUpdatesExistingGVR(t *testing.T) {
	d := &ResourceDiscovery{
		resourceMap: make(map[string]APIResource),
		gvrMap:      make(map[string]schema.GroupVersionResource),
	}

	resource := APIResource{
		Group:      "keda.sh",
		Version:    "v1alpha1",
		Kind:       "ScaledObject",
		Name:       "scaledobjects",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get", "list"},
	}
	d.AddAPIResource(resource)
	resource.Verbs = []string{"get", "list", "watch"}
	d.AddAPIResource(resource)

	resources, err := d.GetAPIResources()
	if err != nil {
		t.Fatalf("GetAPIResources failed: %v", err)
	}
	if len(resources) != 1 {
		t.Fatalf("resource count = %d, want 1", len(resources))
	}
	gvr := schema.GroupVersionResource{Group: "keda.sh", Version: "v1alpha1", Resource: "scaledobjects"}
	if !d.SupportsWatchGVR(gvr) {
		t.Fatal("updated GVR should support watch")
	}
}

func TestSupportsWatchGVRCoreResourceUsesExactEmptyGroup(t *testing.T) {
	d := &ResourceDiscovery{
		resourceMap: make(map[string]APIResource),
		gvrMap:      make(map[string]schema.GroupVersionResource),
	}

	d.AddAPIResource(APIResource{
		Group:      "",
		Version:    "v1",
		Kind:       "Service",
		Name:       "services",
		Namespaced: true,
		IsCRD:      false,
		Verbs:      []string{"get", "list", "watch"},
	})
	d.AddAPIResource(APIResource{
		Group:      "serving.knative.dev",
		Version:    "v1",
		Kind:       "Service",
		Name:       "services",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get", "list"},
	})

	core := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}
	knative := schema.GroupVersionResource{Group: "serving.knative.dev", Version: "v1", Resource: "services"}
	if !d.SupportsWatchGVR(core) {
		t.Fatal("core Service GVR should support watch")
	}
	if d.SupportsWatchGVR(knative) {
		t.Fatal("Knative Service GVR should not inherit watch support from core Service")
	}
}

// TestGetGVRBareKindPrefersListWatchAcrossGroups covers the kueue LocalQueue
// collision: the plural "localqueues" exists as a real CRD in kueue.x-k8s.io
// (supports list/watch) and as kueue's aggregated visibility APIService in
// visibility.kueue.x-k8s.io (no list/watch). Both are CRD-typed and live in
// different groups, so the bare-kind pick is decided by the list/watch verbs:
// GetGVR must resolve to the list/watch-capable CRD regardless of insertion
// order, since cross-group discovery order is unstable in production.
func TestGetGVRBareKindPrefersListWatchAcrossGroups(t *testing.T) {
	visibility := APIResource{
		Group:      "visibility.kueue.x-k8s.io",
		Version:    "v1beta2",
		Kind:       "LocalQueue",
		Name:       "localqueues",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get"},
	}
	realCRD := APIResource{
		Group:      "kueue.x-k8s.io",
		Version:    "v1beta1",
		Kind:       "LocalQueue",
		Name:       "localqueues",
		Namespaced: true,
		IsCRD:      true,
		Verbs:      []string{"get", "list", "watch"},
	}
	want := schema.GroupVersionResource{Group: "kueue.x-k8s.io", Version: "v1beta1", Resource: "localqueues"}

	cases := []struct {
		name  string
		first APIResource
		last  APIResource
	}{
		{name: "visibility discovered first", first: visibility, last: realCRD},
		{name: "real CRD discovered first", first: realCRD, last: visibility},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			d := &ResourceDiscovery{
				resourceMap: make(map[string]APIResource),
				gvrMap:      make(map[string]schema.GroupVersionResource),
			}
			d.AddAPIResource(tc.first)
			d.AddAPIResource(tc.last)

			gvr, ok := d.GetGVR("localqueues")
			if !ok {
				t.Fatal("expected bare-kind localqueues lookup to resolve")
			}
			if gvr != want {
				t.Fatalf("GetGVR(localqueues) = %v, want %v (list/watch-capable CRD)", gvr, want)
			}
		})
	}
}

// A nil *discovery.DiscoveryClient handed to this constructor makes a non-nil
// interface value, so a plain `client == nil` guard passes and the first call
// on the client segfaults. This crashed the whole internal/k8s test binary in
// CI, where discovery starts before any client exists.
func TestNewResourceDiscoveryRejectsATypedNilClient(t *testing.T) {
	var client *discovery.DiscoveryClient // nil, but not a nil interface

	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("constructing with a typed-nil client must return an error, not panic: %v", r)
		}
	}()

	rd, err := NewResourceDiscovery(client)
	if err == nil {
		t.Fatal("expected an error for a nil discovery client")
	}
	if rd != nil {
		t.Errorf("expected no discovery on error, got %#v", rd)
	}
}

func TestNewResourceDiscoveryRejectsAPlainNilClient(t *testing.T) {
	if _, err := NewResourceDiscovery(nil); err == nil {
		t.Fatal("expected an error for a nil discovery client")
	}
}

func TestGetResourceWithGroupPrefersMostStableVersionInEitherOrder(t *testing.T) {
	for _, order := range [][]string{{"v1beta1", "v1beta2"}, {"v1beta2", "v1beta1"}} {
		d := &ResourceDiscovery{
			resourceMap: make(map[string]APIResource),
			gvrMap:      make(map[string]schema.GroupVersionResource),
		}
		for _, version := range order {
			d.AddAPIResource(APIResource{Group: "kueue.x-k8s.io", Version: version, Kind: "ClusterQueue", Name: "clusterqueues", IsCRD: true, Verbs: []string{"get", "list", "watch"}})
		}
		for _, lookup := range []string{"ClusterQueue", "clusterqueues"} {
			res, ok := d.GetResourceWithGroup(lookup, "kueue.x-k8s.io")
			if !ok || res.Version != "v1beta2" || res.Name != "clusterqueues" || res.Namespaced {
				t.Fatalf("discovery order %v, lookup %q: got %+v ok=%v, want cluster-scoped v1beta2", order, lookup, res, ok)
			}
		}
	}
}

// An aggregated API registers each version on its own, so one resource can be
// namespaced at one version and cluster-scoped at another.
func TestNamespacedGVRReadsTheExactVersion(t *testing.T) {
	d := &ResourceDiscovery{
		resourceMap: make(map[string]APIResource),
		gvrMap:      make(map[string]schema.GroupVersionResource),
	}
	d.AddAPIResource(APIResource{Group: "widgets.example.io", Version: "v1beta1", Kind: "Widget", Name: "widgets", Namespaced: true, Verbs: []string{"get", "list", "watch"}})
	d.AddAPIResource(APIResource{Group: "widgets.example.io", Version: "v1", Kind: "Widget", Name: "widgets", Namespaced: false, Verbs: []string{"get", "list", "watch"}})

	for version, want := range map[string]bool{"v1beta1": true, "v1": false} {
		got, known := d.NamespacedGVR(schema.GroupVersionResource{Group: "widgets.example.io", Version: version, Resource: "widgets"})
		if !known || got != want {
			t.Errorf("%s: namespaced=%v known=%v, want %v", version, got, known, want)
		}
	}
	if _, known := d.NamespacedGVR(schema.GroupVersionResource{Group: "widgets.example.io", Version: "v2", Resource: "widgets"}); known {
		t.Error("a version discovery doesn't serve must be unknown")
	}
}