Ctrl K
ComunidadGuías…

internal/metrics/metrics_test.go

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

Archivo original ↗Preguntar a KiBot
package metrics

import (
	"context"
	"errors"
	"maps"
	"strings"
	"testing"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic/fake"
	k8stesting "k8s.io/client-go/testing"
)

func listKinds() map[schema.GroupVersionResource]string {
	return map[schema.GroupVersionResource]string{
		podMetricsGVR:  "PodMetricsList",
		nodeMetricsGVR: "NodeMetricsList",
		nodeGVR:        "NodeList",
	}
}

func obj(apiVersion, kind, name, namespace string, extra map[string]any) *unstructured.Unstructured {
	object := map[string]any{
		"apiVersion": apiVersion,
		"kind":       kind,
		"metadata":   map[string]any{"name": name, "namespace": namespace},
	}
	maps.Copy(object, extra)
	return &unstructured.Unstructured{Object: object}
}

func create(t *testing.T, dyn *fake.FakeDynamicClient, gvr schema.GroupVersionResource, ns string, object *unstructured.Unstructured) {
	t.Helper()
	var err error
	if ns == "" {
		_, err = dyn.Resource(gvr).Create(context.Background(), object, metav1.CreateOptions{})
	} else {
		_, err = dyn.Resource(gvr).Namespace(ns).Create(context.Background(), object, metav1.CreateOptions{})
	}
	if err != nil {
		t.Fatalf("create %s/%s: %v", gvr.Resource, object.GetName(), err)
	}
}

func seed(t *testing.T) *fake.FakeDynamicClient {
	t.Helper()
	scheme := runtime.NewScheme()
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds())
	create(t, dyn, podMetricsGVR, "prod", obj("metrics.k8s.io/v1beta1", "PodMetrics", "web", "prod", map[string]any{
		"containers": []any{
			"not-a-map",
			map[string]any{"name": "no-usage"},
			map[string]any{"name": "a", "usage": map[string]any{"cpu": "100m", "memory": "128Mi"}},
			map[string]any{"name": "b", "usage": map[string]any{"cpu": "50m", "memory": "64Mi"}},
		},
	}))
	create(t, dyn, nodeMetricsGVR, "", obj("metrics.k8s.io/v1beta1", "NodeMetrics", "n1", "", map[string]any{
		"usage": map[string]any{"cpu": "1500m", "memory": "2048Mi"},
	}))
	create(t, dyn, nodeMetricsGVR, "", obj("metrics.k8s.io/v1beta1", "NodeMetrics", "n2", "", map[string]any{
		"usage": map[string]any{"cpu": "500m", "memory": "512Mi"},
	}))
	create(t, dyn, nodeMetricsGVR, "", obj("metrics.k8s.io/v1beta1", "NodeMetrics", "n-nousage", "", map[string]any{}))
	create(t, dyn, nodeGVR, "", obj("v1", "Node", "n1", "", map[string]any{
		"status": map[string]any{"allocatable": map[string]any{"cpu": "4", "memory": "8192Mi"}},
	}))
	create(t, dyn, nodeGVR, "", obj("v1", "Node", "n3", "", map[string]any{
		"status": map[string]any{},
	}))
	return dyn
}

func TestBuild(t *testing.T) {
	dyn := seed(t)

	metrics := Build(context.Background(), dyn, FromCluster(dyn))

	web := metrics.Pods["prod/web"]
	if web.CPUMilli != 150 || web.MemoryMi != 192 {
		t.Fatalf("pod web = %+v, want cpu 150 mem 192", web)
	}

	if len(metrics.Nodes) != 2 {
		t.Fatalf("nodes = %d, want 2 (n-nousage skipped)", len(metrics.Nodes))
	}
	n1 := metrics.Nodes["n1"]
	if n1.CPUMilli != 1500 || n1.MemoryMi != 2048 || n1.CPUPercent != 37 || n1.MemPercent != 25 {
		t.Fatalf("node n1 = %+v", n1)
	}
	n2 := metrics.Nodes["n2"]
	if n2.CPUMilli != 500 || n2.CPUPercent != 0 || n2.MemPercent != 0 {
		t.Fatalf("node n2 = %+v, want no percent", n2)
	}
	if n1.CPUAllocatableMilli != 4000 || n1.MemAllocatableMi != 8192 {
		t.Fatalf("node n1 = %+v, want what the node has to give", n1)
	}
	if n2.CPUAllocatableMilli != 0 || n2.MemAllocatableMi != 0 {
		t.Fatalf("node n2 = %+v, want nothing claimed about a ceiling", n2)
	}
}

func TestBuildListErrors(t *testing.T) {
	scheme := runtime.NewScheme()
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds())
	dyn.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("pods list failed")
	})
	dyn.PrependReactor("list", "nodes", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, errors.New("nodes list failed")
	})
	m := Build(context.Background(), dyn, FromCluster(dyn))
	if len(m.Pods) != 0 {
		t.Fatalf("pods = %d, want 0", len(m.Pods))
	}
	if len(m.Nodes) != 0 {
		t.Fatalf("nodes = %d, want 0", len(m.Nodes))
	}
}

func TestBuildReportsAPanickingMetricsReader(t *testing.T) {
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds())
	dyn.PrependReactor("list", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) {
		if action.GetResource().Group == "metrics.k8s.io" {
			panic("broken metrics client")
		}
		return false, nil, nil
	})

	built := Build(context.Background(), dyn, FromCluster(dyn))

	if !strings.Contains(built.Error, "pods.metrics.k8s.io") {
		t.Fatalf("error = %q, want the panicking reader named", built.Error)
	}
}

func TestNodeAllocatableListError(t *testing.T) {
	dyn := seed(t)
	dyn.PrependReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) {
		if action.GetResource().Group == "" {
			return true, nil, errors.New("core nodes list failed")
		}
		return false, nil, nil
	})
	m := Build(context.Background(), dyn, FromCluster(dyn))
	n1 := m.Nodes["n1"]
	if n1.CPUMilli != 1500 || n1.CPUPercent != 0 {
		t.Fatalf("node n1 = %+v, want usage without percent", n1)
	}
}

func TestQuantityHelpers(t *testing.T) {
	if got := milli(map[string]any{"cpu": "250m"}); got != 250 {
		t.Fatalf("milli valid = %d, want 250", got)
	}
	if got := mebi(map[string]any{"memory": "512Mi"}); got != 512 {
		t.Fatalf("mebi valid = %d, want 512", got)
	}
	if got := milli(map[string]any{"cpu": 5}); got != 0 {
		t.Fatalf("milli non-string = %d, want 0", got)
	}
	if got := mebi(map[string]any{"memory": "bad"}); got != 0 {
		t.Fatalf("mebi parse error = %d, want 0", got)
	}
	if got := milli(map[string]any{}); got != 0 {
		t.Fatalf("milli missing = %d, want 0", got)
	}
}

func TestPercent(t *testing.T) {
	if got := percent(50, 200); got != 25 {
		t.Fatalf("percent(50,200) = %d, want 25", got)
	}
	if got := percent(5, 0); got != 0 {
		t.Fatalf("percent(5,0) = %d, want 0", got)
	}
}

type stubNodes struct {
	items []*unstructured.Unstructured
	err   error
}

func (s stubNodes) List(context.Context) ([]*unstructured.Unstructured, error) {
	return s.items, s.err
}

func TestAllocatableComesFromTheGivenSource(t *testing.T) {
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds())
	create(t, dyn, nodeMetricsGVR, "", obj("metrics.k8s.io/v1beta1", "NodeMetrics", "n1", "", map[string]any{
		"usage": map[string]any{"cpu": "1000m", "memory": "1024Mi"},
	}))
	cached := stubNodes{items: []*unstructured.Unstructured{
		obj("v1", "Node", "n1", "", map[string]any{
			"status": map[string]any{"allocatable": map[string]any{"cpu": "4", "memory": "4096Mi"}},
		}),
	}}

	built := Build(context.Background(), dyn, cached)

	if built.Nodes["n1"].CPUPercent != 25 || built.Nodes["n1"].MemPercent != 25 {
		t.Fatalf("node n1 = %+v, want a quarter of the cached allocatable", built.Nodes["n1"])
	}
}

func TestANodeSourceThatFailsIsReported(t *testing.T) {
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds())
	create(t, dyn, nodeMetricsGVR, "", obj("metrics.k8s.io/v1beta1", "NodeMetrics", "n1", "", map[string]any{
		"usage": map[string]any{"cpu": "1000m", "memory": "1024Mi"},
	}))

	built := Build(context.Background(), dyn, stubNodes{err: errors.New("nodes is forbidden")})

	if built.Error == "" {
		t.Fatal("a refused node listing was swallowed")
	}
	if built.Nodes["n1"].CPUMilli != 1000 {
		t.Fatalf("node n1 = %+v, want the usage without percentages", built.Nodes["n1"])
	}
	if built.Nodes["n1"].CPUPercent != 0 {
		t.Fatalf("cpu percent = %d, want none without allocatable", built.Nodes["n1"].CPUPercent)
	}
}

func notServed(resource string) error {
	return apierrors.NewNotFound(schema.GroupResource{Group: "metrics.k8s.io", Resource: resource}, "")
}

func TestAClusterThatServesNoMetricsAPISaysSoWithoutAnError(t *testing.T) {
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds())
	dyn.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, notServed("pods")
	})
	dyn.PrependReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) {
		if action.GetResource().Group != "metrics.k8s.io" {
			return false, nil, nil
		}
		return true, nil, notServed("nodes")
	})

	got := Build(context.Background(), dyn, FromCluster(dyn))

	if !got.Absent {
		t.Fatal("a cluster with no metrics api was not marked absent")
	}
	if got.Error != "" {
		t.Fatalf("error = %q, want none for a metrics api that is simply not installed", got.Error)
	}
}

func TestAMetricsAPIThatFailsIsStillAFailure(t *testing.T) {
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds())
	dyn.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, notServed("pods")
	})
	dyn.PrependReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) {
		if action.GetResource().Group != "metrics.k8s.io" {
			return false, nil, nil
		}
		return true, nil, errors.New("metrics-server timed out")
	})

	got := Build(context.Background(), dyn, FromCluster(dyn))

	if got.Absent {
		t.Fatal("a metrics api that answered with an error was called absent")
	}
	if !strings.Contains(got.Error, "metrics-server timed out") {
		t.Fatalf("error = %q, want the node metrics failure named", got.Error)
	}
}

func TestOnlyANotFoundAnswerMeansTheMetricsAPIIsNotServed(t *testing.T) {
	if !NotServed(notServed("nodes")) {
		t.Fatal("a not-found answer was not read as a missing metrics api")
	}
	if NotServed(errors.New("the server could not find the requested resource")) {
		t.Fatal("a plain error that only reads like not-found was taken as a missing metrics api")
	}
	if NotServed(nil) {
		t.Fatal("no error was read as a missing metrics api")
	}
}

func TestBuildInReadsOnlyTheNamespaceItWasGiven(t *testing.T) {
	dyn := seed(t)
	create(t, dyn, podMetricsGVR, "dev", obj("metrics.k8s.io/v1beta1", "PodMetrics", "api", "dev", map[string]any{
		"containers": []any{map[string]any{"name": "a", "usage": map[string]any{"cpu": "10m", "memory": "16Mi"}}},
	}))
	dyn.PrependReactor("list", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) {
		if action.GetNamespace() == "" {
			return true, nil, apierrors.NewForbidden(podMetricsGVR.GroupResource(), "", errors.New("cluster scope"))
		}
		return false, nil, nil
	})

	built := BuildIn(context.Background(), dyn, "prod")

	if built.Error != "" {
		t.Fatalf("error = %q, want a namespaced read that needs no cluster-wide list", built.Error)
	}
	if len(built.Pods) != 1 {
		t.Fatalf("pods = %v, want only the one in prod", built.Pods)
	}
	web := built.Pods["prod/web"]
	if web.CPUMilli != 150 || web.MemoryMi != 192 {
		t.Fatalf("pod web = %+v, want cpu 150 mem 192", web)
	}
	if len(built.Nodes) != 0 {
		t.Fatalf("nodes = %v, want none read for one namespace", built.Nodes)
	}
}

func TestBuildInSaysWhenMetricsAreNotServed(t *testing.T) {
	dyn := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds())
	dyn.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
		return true, nil, apierrors.NewNotFound(podMetricsGVR.GroupResource(), "")
	})

	built := BuildIn(context.Background(), dyn, "prod")

	if !built.Absent {
		t.Fatal("absent = false, want metrics-server reported missing")
	}
	if built.Error != "" {
		t.Fatalf("error = %q, want a missing API told apart from a failure", built.Error)
	}
}