Skip to content

Commit 2c675b3

Browse files
L3n41cclaude
andauthored
[CASCL-1304] kubectl-datadog: enrich dd-cluster-info ConfigMap (#2980)
* [CASCL-1304] kubectl-datadog: enrich dd-cluster-info ConfigMap The dd-cluster-info ConfigMap (introduced by #2945) now records: - the running Karpenter installation (version, namespace, ownership) under a new `autoscaling` parent that also groups the existing clusterAutoscaler entry and a new eksAutoMode entry, - a `managedByDatadog` flag per node-management entity (Fargate profile, Karpenter NodePool), so a future migration tool can distinguish Datadog-managed entities to keep from legacy ones to drain. Detection helpers `FindKarpenterInstallation` and `IsEKSAutoModeEnabled` move from `install/guess/` to new `common/karpenter/` and `common/eksautomode/` packages so the clusterinfo classifier can reuse them. A generic `commonk8s.FindFirstDeployment` factors out the shared pager+predicate scan, and `commonk8s.ExtractDeploymentVersion` factors out the controller-image-tag → label fallback used by both detectors. Karpenter NodePool ownership uses the broader `autoscaling.datadoghq.com/created` label only (vs. uninstall's AND-pair with `app.kubernetes.io/managed-by: kubectl-datadog`) so NodePools managed by the Datadog cluster agent are also preserved by the migration tool. Datadog-managed NodePools with no nodes yet (typical right after install) are seeded into the snapshot with an empty Nodes list so the migration tool sees the destination NodePools exist. Fargate profile ownership reads tags via EKS DescribeFargateProfile; the `managed-by: kubectl-datadog` tag is propagated automatically from the CloudFormation stack tags, so no infrastructure change is needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: fix missed call site after merge of #2976 The merge of main into the feature branch (commit 87c6e46) brought in PR #2976's `update.go`, which referenced `guess.FindKarpenterInstallation` — the symbol our PR moved to `karpenter.FindInstallation`. The resolution updated all other call sites but missed this one, so the build broke on the merge commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: pass *Deployment to predicates The pager-driven scan in commonk8s.FindFirstDeployment yields *appsv1.Deployment values out of EachListItem, but the predicate parameter was a value type — forcing a several-hundred-byte struct copy on every iteration. Switch the predicate signature (and ExtractDeploymentVersion) to take a pointer, eliminating the copy in the per-iteration callback. Container-level predicates (isControllerContainer, isClusterAutoscalerContainer) keep value semantics because slices.ContainsFunc requires func(E) bool for []E. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: table-drive FindFirstDeployment tests Collapse the three predicate-driven tests (NoMatch, ReturnsFirstMatch, ShortCircuits) into a single table that pins both the returned Deployment and the predicate-call count per row. The short-circuit invariant becomes a column rather than a dedicated test, and the row shape made it cheap to add three previously-uncovered cases (empty cluster, last deployment matches, multiple matches → first wins). PropagatesListError stays separate: its setup (PrependReactor) and assertions (ErrorIs) diverge from the predicate-counting flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: centralize managed-by tag constants The Datadog ownership tag (`managed-by: kubectl-datadog`) is written on CloudFormation stacks by `aws.buildTags` and propagated by CFN to the EKS Fargate profile resources. The classifier in clusterinfo reads those propagated tags to flag Fargate profiles as ManagedByDatadog. Both ends previously embedded the same string literals; a rename in buildTags would have silently broken classification. Hoist the pair to exported constants `aws.ManagedByTag` / `aws.ManagedByTagValue` and reference them from both writer and reader. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: extract cluster-autoscaler detector to its own package Mirror the karpenter package's shape — Installation type + FindInstallation function + private matchesDeployment / matchesContainer predicates — for the cluster-autoscaler detector, which had stayed inline in clusterinfo. Both detectors now answer "is the X controller running on this cluster?" with the same surface and the same package layout, eliminating the asymmetry that was masking the duplicated fingerprint pattern. The unit-level coverage (label variants, image-substring match, version extraction with image-tag → label fallback, scaled-to-zero treated as absent) moves to the new common/clusterautoscaler/clusterautoscaler_test.go table-driven tests. classify_test.go keeps a single integration smoke test verifying the snapshot wires the detection through. karpenter.go's predicates are renamed for symmetry: matchesController → matchesDeployment, isControllerContainer → matchesContainer. Both packages now expose the same internal vocabulary. clusterautoscaler.Installation has no IsOwn / InstalledBy / InstallerVersion fields, on purpose: kubectl-datadog never installs cluster-autoscaler, only detects it. The asymmetry with karpenter.Installation reflects an actual behavioural asymmetry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: list NodePools via the typed client The kubectl-datadog binary already imports `karpv1 "sigs.k8s.io/karpenter/pkg/apis/v1"` (via cluster/k8s/nodepool.go and cluster/common/clients/clients.go which registers `karpv1.NodePool` and `karpv1.NodePoolList` on the controller-runtime scheme). The justification for going through `unstructured.UnstructuredList` in `enrichKarpenterOwnership` was therefore wrong — replace it with a typed `karpv1.NodePoolList` and drop the unstructured import. The fake controller-runtime client in classify_test.go is updated to mirror the production scheme (typed NodePool/NodePoolList registration). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: drop redundant empty-bucket guard `range` over a nil or empty map is a no-op in Go, so the early-return guard in enrichFargateOwnership was dead code. The function only reads the bucket (it never initialises it), so no side effect is at risk. The analogous guard in enrichKarpenterOwnership stays — it prevents creating an empty NodeManagerKarpenter bucket when no Datadog NodePools exist, which is a load-bearing semantic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: gofmt clusterautoscaler_test.go Struct-field alignment fix that go fmt picked up — the dd-gitlab check_formatting job runs `make fmt && git diff --exit-code`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [CASCL-1304] kubectl-datadog: record cluster ARN and AWS region Per the Slack discussion with Cedric on PR #2980, surface the cluster identifier in the snapshot so a downstream tool can locate the cluster unambiguously (the ARN embeds the AWS account, region and short name). Classify gains a single EKS DescribeCluster call up front, parses the ARN with aws/arn.Parse to derive the region, and populates two new fields on ClusterInfo (`clusterArn` / `region`, both `omitempty`). The EKSDescriber interface grows to expose DescribeCluster alongside the existing DescribeFargateProfile so the test fake can substitute both. Best-effort: a DescribeCluster error or a malformed ARN logs a warning and leaves the fields empty rather than failing the snapshot — same contract as the other autoscaling detectors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 47e8393 commit 2c675b3

16 files changed

Lines changed: 1127 additions & 295 deletions

File tree

cmd/kubectl-datadog/autoscaling/cluster/apply/run.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/pkg/browser"
2222
"helm.sh/helm/v3/pkg/registry"
2323
"k8s.io/cli-runtime/pkg/genericclioptions"
24+
"k8s.io/client-go/discovery"
2425
"k8s.io/client-go/kubernetes"
2526
ctrl "sigs.k8s.io/controller-runtime"
2627
"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -29,7 +30,9 @@ import (
2930
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/clients"
3031
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/clusterinfo"
3132
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/display"
33+
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/eksautomode"
3234
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/helm"
35+
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/karpenter"
3336
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/guess"
3437
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/k8s"
3538
"github.com/DataDog/datadog-operator/pkg/plugin/common"
@@ -107,13 +110,13 @@ func Run(ctx context.Context, streams genericclioptions.IOStreams, configFlags *
107110
log.SetOutput(streams.ErrOut)
108111
ctrl.SetLogger(zap.New(zap.UseDevMode(false), zap.WriteTo(streams.ErrOut)))
109112

110-
if autoModeEnabled, err := guess.IsEKSAutoModeEnabled(clientset.Discovery()); err != nil {
113+
if autoModeEnabled, err := eksautomode.IsEnabled(clientset.Discovery()); err != nil {
111114
return fmt.Errorf("failed to check for EKS auto-mode: %w", err)
112115
} else if autoModeEnabled {
113116
return displayEKSAutoModeMessage(streams, opts.ClusterName)
114117
}
115118

116-
k, err := guess.FindKarpenterInstallation(ctx, clientset)
119+
k, err := karpenter.FindInstallation(ctx, clientset)
117120
if err != nil {
118121
return fmt.Errorf("failed to check for an existing Karpenter installation: %w", err)
119122
}
@@ -149,7 +152,7 @@ func Run(ctx context.Context, streams genericclioptions.IOStreams, configFlags *
149152
return err
150153
}
151154

152-
if err = recordClusterInfo(ctx, cli, opts.ClusterName, opts.KarpenterNamespace); err != nil {
155+
if err = recordClusterInfo(ctx, cli, clientset.Discovery(), opts.ClusterName, opts.KarpenterNamespace); err != nil {
153156
log.Printf("Warning: %v", err)
154157
}
155158

@@ -377,10 +380,10 @@ func karpenterHelmValues(clusterName string, mode InstallMode, irsaRoleArn strin
377380
}`
378381

379382
values := map[string]any{
380-
// See guess.InstalledByLabel for why these keys are Datadog-namespaced.
383+
// See karpenter.InstalledByLabel for why these keys are Datadog-namespaced.
381384
"additionalLabels": map[string]any{
382-
guess.InstalledByLabel: guess.InstalledByValue,
383-
guess.InstallerVersionLabel: version.GetVersion(),
385+
karpenter.InstalledByLabel: karpenter.InstalledByValue,
386+
karpenter.InstallerVersionLabel: version.GetVersion(),
384387
},
385388
"settings": map[string]any{
386389
"clusterName": clusterName,
@@ -463,8 +466,15 @@ func createNodePoolResources(ctx context.Context, streams genericclioptions.IOSt
463466
// recordClusterInfo classifies every node by its current management method
464467
// and writes the snapshot to a ConfigMap. The information is consumed by the
465468
// follow-up migration step.
466-
func recordClusterInfo(ctx context.Context, cli *clients.Clients, clusterName, namespace string) error {
467-
info, err := clusterinfo.Classify(ctx, cli.K8sClientset, cli.Autoscaling, clusterName)
469+
func recordClusterInfo(ctx context.Context, cli *clients.Clients, discoveryClient discovery.DiscoveryInterface, clusterName, namespace string) error {
470+
info, err := clusterinfo.Classify(ctx, clusterinfo.ClassifyInput{
471+
K8sClient: cli.K8sClientset,
472+
CtrlClient: cli.K8sClient,
473+
Autoscaling: cli.Autoscaling,
474+
EKS: cli.EKS,
475+
Discovery: discoveryClient,
476+
ClusterName: clusterName,
477+
})
468478
if err != nil {
469479
return fmt.Errorf("failed to classify cluster nodes: %w", err)
470480
}
@@ -509,7 +519,7 @@ func displayEKSAutoModeMessage(streams genericclioptions.IOStreams, clusterName
509519
return nil
510520
}
511521

512-
func displayForeignKarpenterMessage(streams genericclioptions.IOStreams, clusterName string, foreign *guess.KarpenterInstallation) error {
522+
func displayForeignKarpenterMessage(streams genericclioptions.IOStreams, clusterName string, foreign *karpenter.Installation) error {
513523
coloredURL := openAutoscalingSettingsURL(streams, clusterName)
514524

515525
display.PrintBox(streams.Out,

cmd/kubectl-datadog/autoscaling/cluster/apply/run_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/stretchr/testify/require"
1010
"k8s.io/cli-runtime/pkg/genericclioptions"
1111

12-
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/guess"
12+
"github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/karpenter"
1313
"github.com/DataDog/datadog-operator/pkg/plugin/common"
1414
)
1515

@@ -36,9 +36,9 @@ func TestKarpenterHelmValues(t *testing.T) {
3636

3737
labels, ok := values["additionalLabels"].(map[string]any)
3838
require.True(t, ok, "additionalLabels must be a map")
39-
assert.Equal(t, guess.InstalledByValue, labels[guess.InstalledByLabel],
40-
"installed-by sentinel must match what FindKarpenterInstallation looks for")
41-
assert.Contains(t, labels, guess.InstallerVersionLabel)
39+
assert.Equal(t, karpenter.InstalledByValue, labels[karpenter.InstalledByLabel],
40+
"installed-by sentinel must match what karpenter.FindInstallation looks for")
41+
assert.Contains(t, labels, karpenter.InstallerVersionLabel)
4242

4343
settings, ok := values["settings"].(map[string]any)
4444
require.True(t, ok)
@@ -102,7 +102,7 @@ func TestDisplayForeignKarpenterMessage(t *testing.T) {
102102
out := &bytes.Buffer{}
103103
streams := genericclioptions.IOStreams{Out: out, ErrOut: &bytes.Buffer{}}
104104

105-
foreign := &guess.KarpenterInstallation{Namespace: "karpenter", Name: "karpenter"}
105+
foreign := &karpenter.Installation{Namespace: "karpenter", Name: "karpenter"}
106106
err := displayForeignKarpenterMessage(streams, "my-cluster", foreign)
107107
require.NoError(t, err, "foreign Karpenter is a successful no-op, not an error")
108108

cmd/kubectl-datadog/autoscaling/cluster/common/aws/cloudformation.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ const (
2323
maxWaitDuration = 30 * time.Minute
2424
)
2525

26+
// ManagedByTag and ManagedByTagValue identify CloudFormation stacks owned by
27+
// kubectl-datadog. The pair is also propagated from the stack to its
28+
// resources (e.g. AWS::EKS::FargateProfile) by CloudFormation's tag
29+
// inheritance, so downstream code reading those resource tags can rely on
30+
// the same constants.
31+
const (
32+
ManagedByTag = "managed-by"
33+
ManagedByTagValue = "kubectl-datadog"
34+
)
35+
2636
func CreateOrUpdateStack(ctx context.Context, client *cloudformation.Client, stackName string, templateBody string, params map[string]string, extraTags map[string]string) error {
2737
existing, err := GetStack(ctx, client, stackName)
2838
if err != nil {
@@ -54,7 +64,7 @@ func createOrUpdateStack(ctx context.Context, client *cloudformation.Client, sta
5464
// override any extra entry sharing the same key.
5565
func buildTags(extraTags map[string]string) []types.Tag {
5666
base := map[string]string{
57-
"managed-by": "kubectl-datadog",
67+
ManagedByTag: ManagedByTagValue,
5868
"version": version.GetVersion(),
5969
}
6070
return lo.MapToSlice(lo.Assign(extraTags, base), func(k, v string) types.Tag {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Package clusterautoscaler detects the legacy kubernetes/autoscaler
2+
// cluster-autoscaler controller running on the cluster, mirroring the
3+
// API shape of the karpenter package.
4+
package clusterautoscaler
5+
6+
import (
7+
"context"
8+
"slices"
9+
"strings"
10+
11+
appsv1 "k8s.io/api/apps/v1"
12+
corev1 "k8s.io/api/core/v1"
13+
"k8s.io/client-go/kubernetes"
14+
15+
commonk8s "github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/k8s"
16+
)
17+
18+
// Installation describes a cluster-autoscaler Deployment found on the
19+
// cluster. Version is extracted from the controller image tag with a
20+
// fallback to `app.kubernetes.io/version` labels.
21+
//
22+
// Unlike karpenter.Installation there is no IsOwn / InstalledBy:
23+
// kubectl-datadog never installs cluster-autoscaler, only detects it.
24+
type Installation struct {
25+
Namespace string
26+
Name string
27+
Version string
28+
}
29+
30+
// FindInstallation returns the first cluster-autoscaler Deployment running
31+
// on the cluster, or nil if none. A Deployment scaled to zero replicas is
32+
// treated as absent — the Karpenter migration guide instructs users to
33+
// scale CA to zero before adopting Karpenter, and we want `Present: false`
34+
// in the snapshot in that state.
35+
//
36+
// Detection matches by Deployment name (`cluster-autoscaler`), the
37+
// well-known `app.kubernetes.io/name` / `k8s-app` labels, or a container
38+
// image referencing `cluster-autoscaler`.
39+
func FindInstallation(ctx context.Context, clientset kubernetes.Interface) (*Installation, error) {
40+
dep, err := commonk8s.FindFirstDeployment(ctx, clientset, matchesDeployment)
41+
if err != nil || dep == nil {
42+
return nil, err
43+
}
44+
return &Installation{
45+
Namespace: dep.Namespace,
46+
Name: dep.Name,
47+
Version: commonk8s.ExtractDeploymentVersion(dep, matchesContainer),
48+
}, nil
49+
}
50+
51+
func matchesDeployment(d *appsv1.Deployment) bool {
52+
if d.Spec.Replicas != nil && *d.Spec.Replicas == 0 {
53+
return false
54+
}
55+
if d.Name == "cluster-autoscaler" ||
56+
d.Labels["app.kubernetes.io/name"] == "cluster-autoscaler" ||
57+
d.Labels["k8s-app"] == "cluster-autoscaler" {
58+
return true
59+
}
60+
return slices.ContainsFunc(d.Spec.Template.Spec.Containers, matchesContainer)
61+
}
62+
63+
func matchesContainer(c corev1.Container) bool {
64+
return strings.Contains(c.Image, "cluster-autoscaler")
65+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package clusterautoscaler
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
appsv1 "k8s.io/api/apps/v1"
9+
corev1 "k8s.io/api/core/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/client-go/kubernetes/fake"
12+
)
13+
14+
func deployment(namespace, name string, labels map[string]string, image string) *appsv1.Deployment {
15+
return &appsv1.Deployment{
16+
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name, Labels: labels},
17+
Spec: appsv1.DeploymentSpec{
18+
Template: corev1.PodTemplateSpec{
19+
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "c", Image: image}}},
20+
},
21+
},
22+
}
23+
}
24+
25+
func deploymentWithReplicas(namespace, name string, replicas int32, image string) *appsv1.Deployment {
26+
d := deployment(namespace, name, nil, image)
27+
d.Spec.Replicas = &replicas
28+
return d
29+
}
30+
31+
func TestFindInstallation(t *testing.T) {
32+
for _, tc := range []struct {
33+
name string
34+
deploy *appsv1.Deployment
35+
want *Installation
36+
}{
37+
{
38+
name: "no deployment",
39+
deploy: nil,
40+
want: nil,
41+
},
42+
{
43+
name: "match by name",
44+
deploy: deployment("kube-system", "cluster-autoscaler", nil, "registry.k8s.io/some-image:v1"),
45+
want: &Installation{Namespace: "kube-system", Name: "cluster-autoscaler"},
46+
},
47+
{
48+
name: "match by app.kubernetes.io/name label",
49+
deploy: deployment("autoscaler", "ca-renamed",
50+
map[string]string{"app.kubernetes.io/name": "cluster-autoscaler"},
51+
"registry.k8s.io/foo:v1"),
52+
want: &Installation{Namespace: "autoscaler", Name: "ca-renamed"},
53+
},
54+
{
55+
name: "match by k8s-app label",
56+
deploy: deployment("autoscaler", "ca-renamed",
57+
map[string]string{"k8s-app": "cluster-autoscaler"},
58+
"registry.k8s.io/foo:v1"),
59+
want: &Installation{Namespace: "autoscaler", Name: "ca-renamed"},
60+
},
61+
{
62+
name: "match by image substring (also extracts version)",
63+
deploy: deployment("custom", "scaler", nil, "registry.k8s.io/autoscaling/cluster-autoscaler:v1.30.0"),
64+
want: &Installation{Namespace: "custom", Name: "scaler", Version: "v1.30.0"},
65+
},
66+
{
67+
name: "scaled to zero is treated as absent",
68+
deploy: deploymentWithReplicas("kube-system", "cluster-autoscaler", 0, "registry.k8s.io/autoscaling/cluster-autoscaler:v1.30.0"),
69+
want: nil,
70+
},
71+
{
72+
name: "Karpenter Deployment is not the cluster-autoscaler",
73+
deploy: deployment("dd-karpenter", "karpenter", nil, "public.ecr.aws/karpenter/karpenter:v1.9.0"),
74+
want: nil,
75+
},
76+
} {
77+
t.Run(tc.name, func(t *testing.T) {
78+
cli := fake.NewSimpleClientset()
79+
if tc.deploy != nil {
80+
cli = fake.NewSimpleClientset(tc.deploy)
81+
}
82+
83+
got, err := FindInstallation(t.Context(), cli)
84+
85+
require.NoError(t, err)
86+
assert.Equal(t, tc.want, got)
87+
})
88+
}
89+
}
90+
91+
func TestFindInstallation_Version(t *testing.T) {
92+
for _, tc := range []struct {
93+
name string
94+
deploy *appsv1.Deployment
95+
want string
96+
}{
97+
{
98+
name: "from image tag",
99+
deploy: deployment("kube-system", "cluster-autoscaler", nil, "registry.k8s.io/autoscaling/cluster-autoscaler:v1.30.0"),
100+
want: "v1.30.0",
101+
},
102+
{
103+
name: "image tag wins over label",
104+
deploy: deployment("kube-system", "cluster-autoscaler", map[string]string{"app.kubernetes.io/version": "v9.9.9"}, "registry.k8s.io/autoscaling/cluster-autoscaler:v1.30.0"),
105+
want: "v1.30.0",
106+
},
107+
{
108+
name: "tag with digest suffix",
109+
deploy: deployment("kube-system", "cluster-autoscaler", nil, "registry.k8s.io/autoscaling/cluster-autoscaler:v1.30.0@sha256:abcdef"),
110+
want: "v1.30.0",
111+
},
112+
{
113+
name: "registry with port and tag",
114+
deploy: deployment("kube-system", "cluster-autoscaler", nil, "localhost:5000/cluster-autoscaler:v1.31.0"),
115+
want: "v1.31.0",
116+
},
117+
{
118+
name: "fallback to deployment label when image is digest only",
119+
deploy: deployment("kube-system", "cluster-autoscaler", map[string]string{"app.kubernetes.io/version": "v1.32.0"}, "registry.k8s.io/autoscaling/cluster-autoscaler@sha256:abcdef"),
120+
want: "v1.32.0",
121+
},
122+
{
123+
name: "no tag, no label",
124+
deploy: deployment("kube-system", "cluster-autoscaler", nil, "registry.k8s.io/autoscaling/cluster-autoscaler@sha256:abcdef"),
125+
want: "",
126+
},
127+
{
128+
name: "malformed image falls back to label",
129+
deploy: deployment("kube-system", "cluster-autoscaler", map[string]string{"app.kubernetes.io/version": "v1.99.0"}, "cluster-autoscaler-:::"),
130+
want: "v1.99.0",
131+
},
132+
} {
133+
t.Run(tc.name, func(t *testing.T) {
134+
cli := fake.NewSimpleClientset(tc.deploy)
135+
136+
got, err := FindInstallation(t.Context(), cli)
137+
138+
require.NoError(t, err)
139+
require.NotNil(t, got)
140+
assert.Equal(t, tc.want, got.Version)
141+
})
142+
}
143+
}

0 commit comments

Comments
 (0)