internal/flux/overview_test.go
Rama main · réplica de lectura del repositorio original.
package flux
import (
"context"
"errors"
"testing"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
k8sfake "k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
"github.com/sophotechlabs/spinoza/internal/api"
"github.com/sophotechlabs/spinoza/internal/discovery"
)
func controllerDeployment(name, version string, ready int32) *appsv1.Deployment {
one := int32(1)
return &appsv1.Deployment{
Name: name,
Namespace: "flux-system",
Labels: map[string]string{
"app.kubernetes.io/part-of": "flux",
"app.kubernetes.io/component": name,
"app.kubernetes.io/version": version,
},
Spec: appsv1.DeploymentSpec{Replicas: &one},
Status: appsv1.DeploymentStatus{ReadyReplicas: ready},
}
}
func operatorDeployment(version string) *appsv1.Deployment {
return &appsv1.Deployment{
Name: "flux-operator",
Namespace: "flux-system",
Labels: map[string]string{
"app.kubernetes.io/name": "flux-operator",
"app.kubernetes.io/version": version,
},
}
}
func controllerPod(name, cpu, memory string) *corev1.Pod {
return &corev1.Pod{
Name: name,
Namespace: "flux-system",
Labels: map[string]string{"app.kubernetes.io/part-of": "flux"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{
Name: "manager",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpu),
corev1.ResourceMemory: resource.MustParse(memory),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("1Gi"),
},
},
}}},
}
}
func overviewDescs() map[string]api.ResourceDescriptor {
return map[string]api.ResourceDescriptor{
discovery.Key("kustomize.toolkit.fluxcd.io", "v1", "kustomizations"): {
Group: "kustomize.toolkit.fluxcd.io", Version: "v1",
Resource: "kustomizations", Kind: "Kustomization", Namespaced: true,
},
discovery.Key("source.toolkit.fluxcd.io", "v1", "gitrepositories"): {
Group: "source.toolkit.fluxcd.io", Version: "v1",
Resource: "gitrepositories", Kind: "GitRepository", Namespaced: true,
},
discovery.Key("fluxcd.controlplane.io", "v1", "fluxinstances"): {
Group: "fluxcd.controlplane.io", Version: "v1",
Resource: "fluxinstances", Kind: "FluxInstance", Namespaced: true,
},
}
}
func overviewKinds() map[schema.GroupVersionResource]string {
return map[schema.GroupVersionResource]string{
{Group: "kustomize.toolkit.fluxcd.io", Version: "v1", Resource: "kustomizations"}: "KustomizationList",
{Group: "source.toolkit.fluxcd.io", Version: "v1", Resource: "gitrepositories"}: "GitRepositoryList",
{Group: "fluxcd.controlplane.io", Version: "v1", Resource: "fluxinstances"}: "FluxInstanceList",
}
}
func syncKustomization() *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "kustomize.toolkit.fluxcd.io/v1",
"kind": "Kustomization",
"metadata": map[string]any{"name": "flux-system", "namespace": "flux-system"},
"spec": map[string]any{
"path": "./clusters/p-mk2",
"sourceRef": map[string]any{"kind": "GitRepository", "name": "flux-system"},
},
"status": map[string]any{
"lastAppliedRevision": "refs/heads/main@sha1:abc",
"conditions": []any{map[string]any{"type": "Ready", "status": "True"}},
},
}}
}
func syncSourceRepo() *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "source.toolkit.fluxcd.io/v1",
"kind": "GitRepository",
"metadata": map[string]any{"name": "flux-system", "namespace": "flux-system"},
"spec": map[string]any{
"url": "ssh://[email protected]/sophotechlabs/hetzner-gitops",
"ref": map[string]any{"name": "refs/heads/main"},
},
}}
}
func fluxInstanceCR() *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fluxcd.controlplane.io/v1",
"kind": "FluxInstance",
"metadata": map[string]any{"name": "flux", "namespace": "flux-system"},
"spec": map[string]any{"distribution": map[string]any{"version": "2.9.4"}},
}}
}
func overviewFor(
t *testing.T,
objects []runtime.Object,
crs []runtime.Object,
usage map[string]api.ResourceUsage,
) api.FluxOverview {
t.Helper()
cs := k8sfake.NewClientset(objects...)
dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
runtime.NewScheme(), overviewKinds(), crs...,
)
return Overview(context.Background(), cs, listerFor(dyn), overviewDescs(), Cluster{
Kubernetes: "v1.36.2+k3s1",
Nodes: 1,
Usage: usage,
})
}
func TestOverviewReadsTheControllersAndTheirVersion(t *testing.T) {
got := overviewFor(t, []runtime.Object{
controllerDeployment("source-controller", "v2.9.4", 1),
controllerDeployment("kustomize-controller", "v2.9.4", 1),
}, nil, nil)
if len(got.Controllers) != 2 {
t.Fatalf("controllers = %+v, want two", got.Controllers)
}
if got.Distribution != "v2.9.4" {
t.Fatalf("distribution = %q, want the shared controller version", got.Distribution)
}
if got.Namespace != "flux-system" {
t.Fatalf("namespace = %q", got.Namespace)
}
if !got.Ready {
t.Fatalf("summary = %q, want a ready cluster", got.Summary)
}
}
func TestOverviewNamesAControllerThatIsShort(t *testing.T) {
got := overviewFor(t, []runtime.Object{
controllerDeployment("source-controller", "v2.9.4", 1),
controllerDeployment("helm-controller", "v2.9.4", 0),
}, nil, nil)
if got.Ready {
t.Fatal("a cluster with a controller down reported ready")
}
if got.Summary != "helm-controller is not ready" {
t.Fatalf("summary = %q", got.Summary)
}
}
func TestOverviewLeavesTheOperatorOutWhenItIsNotInstalled(t *testing.T) {
got := overviewFor(t, []runtime.Object{
controllerDeployment("source-controller", "v2.9.4", 1),
}, nil, nil)
if got.Operator != "" {
t.Fatalf("operator = %q, want nothing on plain flux", got.Operator)
}
if got.Distribution != "v2.9.4" {
t.Fatalf("distribution = %q, want the version the controllers carry", got.Distribution)
}
}
func TestOverviewReportsTheOperatorWhenItIsThere(t *testing.T) {
got := overviewFor(t, []runtime.Object{
controllerDeployment("source-controller", "v2.9.4", 1),
operatorDeployment("v0.58.0"),
}, []runtime.Object{fluxInstanceCR()}, nil)
if got.Operator != "v0.58.0" {
t.Fatalf("operator = %q, want v0.58.0", got.Operator)
}
if got.Distribution != "2.9.4" {
t.Fatalf("distribution = %q, want what the instance asks for", got.Distribution)
}
}
func TestOverviewFollowsTheSyncToItsSource(t *testing.T) {
got := overviewFor(
t,
[]runtime.Object{controllerDeployment("kustomize-controller", "v2.9.4", 1)},
[]runtime.Object{syncKustomization(), syncSourceRepo()},
nil,
)
if got.Sync.Kind != "Kustomization" || got.Sync.Name != "flux-system" {
t.Fatalf("sync = %+v", got.Sync)
}
if got.Sync.URL != "ssh://[email protected]/sophotechlabs/hetzner-gitops" {
t.Fatalf("url = %q", got.Sync.URL)
}
if got.Sync.Ref != "refs/heads/main" {
t.Fatalf("ref = %q", got.Sync.Ref)
}
if got.Sync.Path != "./clusters/p-mk2" {
t.Fatalf("path = %q", got.Sync.Path)
}
if got.Sync.Revision != "refs/heads/main@sha1:abc" {
t.Fatalf("revision = %q", got.Sync.Revision)
}
if !got.Sync.Ready {
t.Fatal("a Ready=True sync reported not ready")
}
}
func TestOverviewFindsTheSyncTheOperatorCreated(t *testing.T) {
got := overviewFor(
t,
[]runtime.Object{
controllerDeployment("kustomize-controller", "v2.9.4", 1),
operatorDeployment("v0.58.0"),
},
[]runtime.Object{fluxInstanceCR(), syncKustomization(), syncSourceRepo()},
nil,
)
if got.Sync.Name != "flux-system" {
t.Fatalf("sync = %+v, want the flux-system kustomization, not the instance name", got.Sync)
}
if got.Sync.Revision != "refs/heads/main@sha1:abc" {
t.Fatalf("revision = %q", got.Sync.Revision)
}
}
func TestOverviewFollowsTheSyncNameTheInstanceAsksFor(t *testing.T) {
instance := fluxInstanceCR()
spec, ok := instance.Object["spec"].(map[string]any)
if !ok {
t.Fatal("the instance fixture has no spec")
}
spec["sync"] = map[string]any{"name": "elsewhere"}
got := overviewFor(
t,
[]runtime.Object{controllerDeployment("kustomize-controller", "v2.9.4", 1)},
[]runtime.Object{instance, syncKustomization()},
nil,
)
if got.Sync.Name != "elsewhere" {
t.Fatalf("sync name = %q, want the one the instance names", got.Sync.Name)
}
if got.Sync.Kind != "" {
t.Fatalf("sync = %+v, want nothing found under that name", got.Sync)
}
}
func TestOverviewSaysWhenThereIsNoSync(t *testing.T) {
got := overviewFor(
t,
[]runtime.Object{controllerDeployment("kustomize-controller", "v2.9.4", 1)},
nil, nil,
)
if got.Sync.Kind != "" {
t.Fatalf("sync = %+v, want none", got.Sync)
}
if !got.Ready {
t.Fatal("a cluster with healthy controllers and no sync should still read as ready")
}
if got.Summary != "the controllers are ready; no cluster sync was found" {
t.Fatalf("summary = %q", got.Summary)
}
}
func TestOverviewAddsUpControllerUsageAgainstRequests(t *testing.T) {
got := overviewFor(
t,
[]runtime.Object{
controllerDeployment("source-controller", "v2.9.4", 1),
controllerPod("source-controller-abc", "100m", "64Mi"),
controllerPod("kustomize-controller-def", "100m", "64Mi"),
},
nil,
map[string]api.ResourceUsage{
"flux-system/source-controller-abc": {CPUMilli: 20, MemoryMi: 100},
"flux-system/kustomize-controller-def": {CPUMilli: 11, MemoryMi: 40},
},
)
if !got.Usage.Known {
t.Fatal("usage came back unknown even though metrics were given")
}
if got.Usage.CPUMilli != 31 || got.Usage.MemoryMi != 140 {
t.Fatalf("usage = %+v", got.Usage)
}
if got.Usage.CPURequestMilli != 200 || got.Usage.MemRequestMi != 128 {
t.Fatalf("requests = %+v", got.Usage)
}
if got.Usage.MemLimitMi != 2048 {
t.Fatalf("limits = %+v", got.Usage)
}
}
func TestOverviewLeavesUsageUnknownWithoutMetrics(t *testing.T) {
got := overviewFor(t, []runtime.Object{
controllerDeployment("source-controller", "v2.9.4", 1),
controllerPod("source-controller-abc", "100m", "64Mi"),
}, nil, nil)
if got.Usage.Known {
t.Fatal("usage was reported without metrics")
}
}
func TestOverviewIsEmptyOnAClusterWithoutFlux(t *testing.T) {
got := overviewFor(t, nil, nil, nil)
if len(got.Controllers) != 0 {
t.Fatalf("controllers = %+v, want none", got.Controllers)
}
if got.Error != "" {
t.Fatalf("error = %q, want none for a cluster that simply has no flux", got.Error)
}
}
func TestOverviewReportsARefusedControllerList(t *testing.T) {
cs := k8sfake.NewClientset()
cs.PrependReactor("list", "deployments", func(k8stesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("deployments is forbidden")
})
dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), overviewKinds())
got := Overview(context.Background(), cs, listerFor(dyn), overviewDescs(), Cluster{})
if got.Error == "" {
t.Fatal("a refused list came back clean")
}
}
type emptyLister struct{}
func (emptyLister) List(context.Context, api.ResourceDescriptor) ([]*unstructured.Unstructured, error) {
return nil, nil
}
func (emptyLister) Warm(context.Context, []api.ResourceDescriptor) {}
type failingLister struct{}
func (failingLister) List(context.Context, api.ResourceDescriptor) ([]*unstructured.Unstructured, error) {
return nil, errors.New("the cache could not be read")
}
func (failingLister) Warm(context.Context, []api.ResourceDescriptor) {}
func TestWantedReplicasFallsBackToOne(t *testing.T) {
if got := wantedReplicas(nil); got != 1 {
t.Fatalf("wanted = %d, want 1 when the deployment names no replica count", got)
}
three := int32(3)
if got := wantedReplicas(&three); got != 3 {
t.Fatalf("wanted = %d, want 3", got)
}
}
func TestSharedVersionOnlyAnswersWhenEveryControllerAgrees(t *testing.T) {
cases := []struct {
name string
controllers []api.FluxController
want string
}{
{name: "none at all", controllers: nil, want: ""},
{
name: "one that never said",
controllers: []api.FluxController{{Name: "source-controller"}},
want: "",
},
{
name: "an unversioned one alongside a versioned one",
controllers: []api.FluxController{
{Name: "source-controller"},
{Name: "kustomize-controller", Version: "v2.7.1"},
},
want: "v2.7.1",
},
{
name: "two that disagree",
controllers: []api.FluxController{
{Name: "source-controller", Version: "v2.7.1"},
{Name: "kustomize-controller", Version: "v2.6.0"},
},
want: "",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := sharedVersion(tc.controllers); got != tc.want {
t.Fatalf("version = %q, want %q", got, tc.want)
}
})
}
}
func TestFluxInstanceIsNilWhenTheKindWasNeverDiscovered(t *testing.T) {
descs := map[string]api.ResourceDescriptor{
discovery.Key("apps", "v1", "deployments"): {Group: "apps", Version: "v1", Resource: "deployments"},
}
if got := fluxInstance(context.Background(), emptyLister{}, descs); got != nil {
t.Fatalf("instance = %+v, want nil without the instance kind", got)
}
}
func TestSyncIsEmptyWithoutTheKustomizationKind(t *testing.T) {
descs := map[string]api.ResourceDescriptor{}
sync := syncOf(context.Background(), emptyLister{}, descs, "flux-system", "flux-system")
if sync.Kind != "" {
t.Fatalf("kind = %q, want empty without the kustomization kind", sync.Kind)
}
if sync.Namespace != "flux-system" || sync.Name != "flux-system" {
t.Fatalf("sync = %+v, want it to still name what it looked for", sync)
}
}
func TestSyncIsEmptyWhenKustomizationsCannotBeListed(t *testing.T) {
sync := syncOf(t.Context(), failingLister{}, overviewDescs(), "flux-system", "flux-system")
if sync.Kind != "" || sync.Namespace != "flux-system" || sync.Name != "flux-system" {
t.Fatalf("sync = %+v, want only the requested identity", sync)
}
}
func TestSyncSourceNeedsBothAKindAndAName(t *testing.T) {
entry := &unstructured.Unstructured{Object: map[string]any{
"spec": map[string]any{"sourceRef": map[string]any{"kind": "GitRepository"}},
}}
kind, url, ref := syncSource(context.Background(), emptyLister{}, nil, entry, "flux-system")
if kind != "" || url != "" || ref != "" {
t.Fatalf("source = %q %q %q, want nothing without a source name", kind, url, ref)
}
}
func TestSyncSourceKeepsTheKindWhenTheSourceCannotBeRead(t *testing.T) {
entry := &unstructured.Unstructured{Object: map[string]any{
"spec": map[string]any{
"sourceRef": map[string]any{"kind": "GitRepository", "name": "flux-system"},
},
}}
descs := map[string]api.ResourceDescriptor{
discovery.Key("source.toolkit.fluxcd.io", "v1", "gitrepositories"): {
Group: "source.toolkit.fluxcd.io",
Version: "v1",
Resource: "gitrepositories",
},
}
kind, url, ref := syncSource(context.Background(), failingLister{}, descs, entry, "flux-system")
if kind != "GitRepository" {
t.Fatalf("kind = %q, want the kind the sync named", kind)
}
if url != "" || ref != "" {
t.Fatalf("url/ref = %q %q, want nothing when the source could not be read", url, ref)
}
}
func TestSyncSourceKeepsTheKindWhenDiscoveryHasNoSourceType(t *testing.T) {
entry := &unstructured.Unstructured{Object: map[string]any{
"spec": map[string]any{
"sourceRef": map[string]any{"kind": "Bucket", "name": "artifacts"},
},
}}
kind, url, ref := syncSource(t.Context(), emptyLister{}, nil, entry, "flux-system")
if kind != "Bucket" || url != "" || ref != "" {
t.Fatalf("source = %q %q %q, want only Bucket", kind, url, ref)
}
}
func TestRefOfHasNothingToSayWithoutARef(t *testing.T) {
source := &unstructured.Unstructured{Object: map[string]any{"spec": map[string]any{}}}
if got := refOf(source); got != "" {
t.Fatalf("ref = %q, want empty", got)
}
}
func TestReadyConditionIgnoresWhatIsNotACondition(t *testing.T) {
item := &unstructured.Unstructured{Object: map[string]any{
"status": map[string]any{
"conditions": []any{
"not a condition",
map[string]any{"type": "Reconciling", "status": "True"},
map[string]any{"type": "Ready", "status": "True"},
},
},
}}
if !readyCondition(item) {
t.Fatal("ready = false, want the Ready condition to be found past the noise")
}
}
func TestUsageStaysUnknownWhenTheControllerPodsCannotBeListed(t *testing.T) {
cs := k8sfake.NewClientset()
cs.PrependReactor("list", "pods", func(k8stesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("pods are forbidden")
})
usage := map[string]api.ResourceUsage{"flux-system/source-controller-0": {CPUMilli: 5}}
result := usageOf(context.Background(), cs, "flux-system", usage)
if result.Known {
t.Fatalf("usage = %+v, want it unknown when the pods could not be listed", result)
}
}
func TestVerdictNamesAnUnreadySync(t *testing.T) {
overview := api.FluxOverview{
Controllers: []api.FluxController{{Name: "source-controller", Ready: true}},
Sync: api.FluxSync{Kind: "Kustomization", Ready: false},
}
ready, summary := verdict(overview)
if ready {
t.Fatal("ready = true, want false while the sync is not ready")
}
if summary != "the cluster sync is not ready" {
t.Fatalf("summary = %q", summary)
}
}
func TestASyncWhoseSourceIsMissingStillNamesTheKind(t *testing.T) {
got := overviewFor(
t,
[]runtime.Object{controllerDeployment("kustomize-controller", "v2.9.4", 1)},
[]runtime.Object{syncKustomization()},
nil,
)
if got.Sync.Name != "flux-system" {
t.Fatalf("sync = %+v, want the kustomization itself", got.Sync)
}
if got.Sync.Source != "GitRepository" {
t.Fatalf("source = %q, want the kind it points at", got.Sync.Source)
}
if got.Sync.URL != "" {
t.Fatalf("url = %q, want nothing when the source is not there", got.Sync.URL)
}
}
func TestASyncThatIsNotReadySaysSo(t *testing.T) {
notReady := syncKustomization()
status, ok := notReady.Object["status"].(map[string]any)
if !ok {
t.Fatal("the fixture lost its status")
}
status["conditions"] = []any{
map[string]any{"type": "Ready", "status": "False"},
}
got := overviewFor(
t,
[]runtime.Object{controllerDeployment("kustomize-controller", "v2.9.4", 1)},
[]runtime.Object{notReady, syncSourceRepo()},
nil,
)
if got.Sync.Ready {
t.Fatal("a Ready=False sync was reported ready")
}
}
func TestASyncWithNoReadyConditionAtAllIsNotReady(t *testing.T) {
silent := syncKustomization()
status, ok := silent.Object["status"].(map[string]any)
if !ok {
t.Fatal("the fixture lost its status")
}
status["conditions"] = []any{
map[string]any{"type": "Reconciling", "status": "True"},
}
got := overviewFor(
t,
[]runtime.Object{controllerDeployment("kustomize-controller", "v2.9.4", 1)},
[]runtime.Object{silent, syncSourceRepo()},
nil,
)
if got.Sync.Ready {
t.Fatal("a sync that never said Ready was reported ready")
}
}