From 53c73887d03e4c8c2dfe6bae2531d7b867446270 Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Fri, 16 Jan 2026 12:01:49 -0800 Subject: [PATCH 1/2] feat(recommended labels): Set recommended labels Set recommended labels as per https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ --- pkg/controller/ippool/pool_controller_test.go | 4 +- .../managed_cluster_controller.go | 2 +- .../logstorage/secrets/secret_controller.go | 2 +- .../logstorage/users/users_controller.go | 2 +- .../monitor/monitor_controller_test.go | 11 +- .../secrets/cluster_ca_controller.go | 2 +- pkg/controller/secrets/tenant_controller.go | 2 +- pkg/controller/utils/component.go | 112 +++++++++++++- pkg/controller/utils/component_test.go | 140 ++++++++++++++---- test/pool_test.go | 15 +- 10 files changed, 239 insertions(+), 53 deletions(-) diff --git a/pkg/controller/ippool/pool_controller_test.go b/pkg/controller/ippool/pool_controller_test.go index 67e25e529d..5cf669d976 100644 --- a/pkg/controller/ippool/pool_controller_test.go +++ b/pkg/controller/ippool/pool_controller_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2025 Tigera, Inc. All rights reserved. +// Copyright (c) 2023-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -258,7 +258,7 @@ var _ = Describe("IP Pool controller tests", func() { } for _, pool := range instance.Spec.CalicoNetwork.IPPools { Expect(poolsByCIDR).To(HaveKey(pool.CIDR)) - Expect(poolsByCIDR[pool.CIDR].Labels).To(Equal(map[string]string{"app.kubernetes.io/managed-by": "tigera-operator"})) + Expect(poolsByCIDR[pool.CIDR].Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", "tigera-operator")) } }) diff --git a/pkg/controller/logstorage/managedcluster/managed_cluster_controller.go b/pkg/controller/logstorage/managedcluster/managed_cluster_controller.go index fe801f47c9..e273ea34e8 100644 --- a/pkg/controller/logstorage/managedcluster/managed_cluster_controller.go +++ b/pkg/controller/logstorage/managedcluster/managed_cluster_controller.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2022-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/controller/logstorage/secrets/secret_controller.go b/pkg/controller/logstorage/secrets/secret_controller.go index 259dda2d0c..83421f73c6 100644 --- a/pkg/controller/logstorage/secrets/secret_controller.go +++ b/pkg/controller/logstorage/secrets/secret_controller.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2023-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/controller/logstorage/users/users_controller.go b/pkg/controller/logstorage/users/users_controller.go index 2f6865d2fc..85ec4268e2 100644 --- a/pkg/controller/logstorage/users/users_controller.go +++ b/pkg/controller/logstorage/users/users_controller.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2023-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/controller/monitor/monitor_controller_test.go b/pkg/controller/monitor/monitor_controller_test.go index 6960bcea18..1a3e7e007f 100644 --- a/pkg/controller/monitor/monitor_controller_test.go +++ b/pkg/controller/monitor/monitor_controller_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2021-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -259,7 +259,14 @@ var _ = Describe("Monitor controller tests", func() { Expect(serviceMonitor.Spec.Endpoints).To(HaveLen(1)) // Verify that the default settings are propagated. - Expect(serviceMonitor.Labels).To(Equal(map[string]string{render.AppLabelName: monitor.TigeraExternalPrometheus})) + Expect(serviceMonitor.Labels).To(Equal(map[string]string{ + render.AppLabelName: monitor.TigeraExternalPrometheus, + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/name": "tigera-external-prometheus", + "app.kubernetes.io/part-of": "Calico", + "app.kubernetes.io/component": "Monitor.operator.tigera.io", + })) Expect(serviceMonitor.Spec.Endpoints[0]).To(Equal(monitoringv1.Endpoint{ Params: map[string][]string{"match[]": {"{__name__=~\".+\"}"}}, Port: "web", diff --git a/pkg/controller/secrets/cluster_ca_controller.go b/pkg/controller/secrets/cluster_ca_controller.go index cc88153259..408956a782 100644 --- a/pkg/controller/secrets/cluster_ca_controller.go +++ b/pkg/controller/secrets/cluster_ca_controller.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2023-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/controller/secrets/tenant_controller.go b/pkg/controller/secrets/tenant_controller.go index 95dd1da8d0..4dced2e779 100644 --- a/pkg/controller/secrets/tenant_controller.go +++ b/pkg/controller/secrets/tenant_controller.go @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2024 Tigera, Inc. All rights reserved. +// Copyright (c) 2023-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/controller/utils/component.go b/pkg/controller/utils/component.go index c3bdc3fea1..1923282d00 100644 --- a/pkg/controller/utils/component.go +++ b/pkg/controller/utils/component.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2025 Tigera, Inc. All rights reserved. +// Copyright (c) 2020-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import ( "context" "fmt" "reflect" + "regexp" "slices" "strings" "sync" @@ -235,7 +236,7 @@ func (c *componentHandler) createOrUpdateObject(ctx context.Context, obj client. setProbeTimeouts(obj) // Make sure we have our standard selector and pod labels - setStandardSelectorAndLabels(obj) + setStandardSelectorAndLabels(obj, c.cr) if err := ensureTLSCiphers(ctx, obj, c.client); err != nil { return fmt.Errorf("failed to set TLS Ciphers: %w", err) @@ -951,16 +952,31 @@ func setProbeTimeouts(obj client.Object) { } } -// setStandardSelectorAndLabels will set the k8s-app and app.kubernetes.io/name Labels on the podTemplates +// setStandardSelectorAndLabels will set the recommended labels found at +// https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ +// It will also set the k8s-app and app.kubernetes.io/name Labels on the podTemplates // for Deployments and Daemonsets. If there is no Selector specified a selector will also be added // that selects the k8s-app label. -func setStandardSelectorAndLabels(obj client.Object) { +func setStandardSelectorAndLabels(obj client.Object, customResource metav1.Object) { + if obj.GetLabels() == nil { + obj.SetLabels(make(map[string]string)) + } + if customResource != nil { + // We do not want to set these labels on objects without a CR. They are usually deliberately not getting an + // owner ref and are not controlled by our operator. + addNameLabel(obj, obj.GetName()) + addInstanceLabel(obj, customResource) + addComponentLabel(obj, customResource) + addPartOfLabel(obj) + addManagedByLabel(obj) + } + var podTemplate *v1.PodTemplateSpec var name string switch obj := obj.(type) { case *apps.Deployment: d := obj - name = d.Name + name = sanitizeLabel(d.Name) if d.Labels == nil { d.Labels = make(map[string]string) } @@ -976,7 +992,7 @@ func setStandardSelectorAndLabels(obj client.Object) { podTemplate = &d.Spec.Template case *apps.DaemonSet: d := obj - name = d.Name + name = sanitizeLabel(d.Name) if d.Spec.Selector == nil { d.Spec.Selector = &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -985,6 +1001,22 @@ func setStandardSelectorAndLabels(obj client.Object) { } } podTemplate = &d.Spec.Template + case *monitoringv1.Prometheus: + d := obj + if d.Spec.PodMetadata == nil { + d.Spec.PodMetadata = &monitoringv1.EmbeddedObjectMetadata{} + } + for k, v := range d.Labels { + d.Spec.PodMetadata.Labels[k] = v + } + case *monitoringv1.Alertmanager: + d := obj + if d.Spec.PodMetadata == nil { + d.Spec.PodMetadata = &monitoringv1.EmbeddedObjectMetadata{} + } + for k, v := range d.Labels { + d.Spec.PodMetadata.Labels[k] = v + } default: return } @@ -995,8 +1027,72 @@ func setStandardSelectorAndLabels(obj client.Object) { if podTemplate.Labels["k8s-app"] == "" { podTemplate.Labels["k8s-app"] = name } - if podTemplate.Labels["app.kubernetes.io/name"] == "" { - podTemplate.Labels["app.kubernetes.io/name"] = name + if customResource != nil { + // We do not want to set these labels on objects without a CR. They are usually deliberately not getting an + // owner ref and are not controlled by our operator. + addNameLabel(podTemplate, obj.GetName()) + addInstanceLabel(podTemplate, customResource) + addComponentLabel(podTemplate, customResource) + addPartOfLabel(podTemplate) + addManagedByLabel(podTemplate) + } + +} + +// sanitizeLabel cleans an input string to conform to the validation for labels. A valid label must be an empty string +// or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character and it +// is validated with regex '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?'. +func sanitizeLabel(input string) string { + sanitized := regexp.MustCompile(`[^a-zA-Z0-9_.-]`).ReplaceAllString(input, "_") + return strings.Trim(sanitized, "-_.") +} + +// addNameLabel sets the name of the application. +// For more on recommended labels see: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ +func addNameLabel(obj metav1.Object, name string) { + if obj.GetLabels()["app.kubernetes.io/name"] == "" { + obj.GetLabels()["app.kubernetes.io/name"] = sanitizeLabel(name) + } + if obj.GetLabels()["k8s-app"] == "" { + obj.GetLabels()["k8s-app"] = sanitizeLabel(name) + } +} + +// addInstanceLabel sets a unique name identifying the instance of an application. We use the name of the custom resource +// that owns this object. +// For more on recommended labels see: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ +func addInstanceLabel(obj metav1.Object, cr metav1.Object) { + if obj.GetLabels()["app.kubernetes.io/instance"] == "" && cr != nil { + obj.GetLabels()["app.kubernetes.io/instance"] = sanitizeLabel(cr.GetName()) + } +} + +// addComponentLabel sets the component within the architecture. We use the kind of the custom resource that owns this +// object. +// For more on recommended labels see: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ +func addComponentLabel(obj metav1.Object, cr metav1.Object) { + if obj.GetLabels()["app.kubernetes.io/component"] == "" && cr != nil { + owner, ok := cr.(runtime.Object) + if ok && owner.GetObjectKind() != nil && owner.GetObjectKind() != nil { + obj.GetLabels()["app.kubernetes.io/component"] = sanitizeLabel(owner.GetObjectKind().GroupVersionKind().GroupKind().String()) + + } + } +} + +// addPartOfLabel sets the name of a higher level application this one is part of. We use the product variant. +// For more on recommended labels see: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ +func addPartOfLabel(obj metav1.Object) { + if obj.GetLabels()["app.kubernetes.io/part-of"] == "" { + obj.GetLabels()["app.kubernetes.io/part-of"] = "Calico" + } +} + +// addManagedByLabel sets the tool being used to manage the operation of an application. +// For more on recommended labels see: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ +func addManagedByLabel(obj metav1.Object) { + if obj.GetLabels()["app.kubernetes.io/managed-by"] == "" { + obj.GetLabels()["app.kubernetes.io/managed-by"] = sanitizeLabel(common.OperatorName()) } } diff --git a/pkg/controller/utils/component_test.go b/pkg/controller/utils/component_test.go index 01a277c151..7682546512 100644 --- a/pkg/controller/utils/component_test.go +++ b/pkg/controller/utils/component_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2025 Tigera, Inc. All rights reserved. +// Copyright (c) 2020-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -414,7 +414,13 @@ var _ = Describe("Component handler tests", func() { By("checking that the namespace is created and desired label is present") expectedLabels := map[string]string{ - fakeComponentLabelKey: fakeComponentLabelValue, + fakeComponentLabelKey: fakeComponentLabelValue, + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/part-of": "Calico", + "app.kubernetes.io/name": "test-namespace", + "k8s-app": "test-namespace", + "app.kubernetes.io/component": "Manager.operator.tigera.io", } nsKey := client.ObjectKey{ Name: "test-namespace", @@ -463,8 +469,14 @@ var _ = Describe("Component handler tests", func() { By("retrieving the namespace and checking that both current and desired labels are still present") expectedLabels = map[string]string{ - "extra": "extra-value", - fakeComponentLabelKey: fakeComponentLabelValue, + "extra": "extra-value", + fakeComponentLabelKey: fakeComponentLabelValue, + "k8s-app": "test-namespace", + "app.kubernetes.io/component": "Manager.operator.tigera.io", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/name": "test-namespace", + "app.kubernetes.io/part-of": "Calico", } ns = &corev1.Namespace{} err = c.Get(ctx, nsKey, ns) @@ -473,9 +485,15 @@ var _ = Describe("Component handler tests", func() { By("changing a desired label") labels = map[string]string{ - "extra": "extra-value", - "cattle-not-pets": "indeed", - fakeComponentLabelKey: "not-present", + "extra": "extra-value", + "cattle-not-pets": "indeed", + fakeComponentLabelKey: "not-present", + "app.kubernetes.io/part-of": "Calico", + "k8s-app": "test-namespace", + "app.kubernetes.io/name": "test-namespace", + "app.kubernetes.io/component": "Manager.operator.tigera.io", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/instance": "tigera-secure", } ns.Labels = labels err = c.Update(ctx, ns) @@ -483,9 +501,15 @@ var _ = Describe("Component handler tests", func() { By("checking that the namespace is updated with new modified label") expectedLabels = map[string]string{ - "cattle-not-pets": "indeed", - "extra": "extra-value", - fakeComponentLabelKey: "not-present", + "cattle-not-pets": "indeed", + "extra": "extra-value", + fakeComponentLabelKey: "not-present", + "app.kubernetes.io/component": "Manager.operator.tigera.io", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/name": "test-namespace", + "app.kubernetes.io/part-of": "Calico", + "k8s-app": "test-namespace", } nsKey = client.ObjectKey{ Name: "test-namespace", @@ -515,9 +539,15 @@ var _ = Describe("Component handler tests", func() { By("retrieving the namespace and checking that desired label is reconciled, everything else is left as-is") expectedLabels = map[string]string{ - "cattle-not-pets": "indeed", - "extra": "extra-value", - fakeComponentLabelKey: fakeComponentLabelValue, + "cattle-not-pets": "indeed", + "extra": "extra-value", + fakeComponentLabelKey: fakeComponentLabelValue, + "app.kubernetes.io/component": "Manager.operator.tigera.io", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/name": "test-namespace", + "k8s-app": "test-namespace", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/part-of": "Calico", } ns = &corev1.Namespace{} err = c.Get(ctx, nsKey, ns) @@ -1198,7 +1228,13 @@ var _ = Describe("Component handler tests", func() { ObjectMeta: metav1.ObjectMeta{ Name: "my-service", Labels: map[string]string{ - "old": "should-be-preserved", + "old": "should-be-preserved", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/name": "my-service", + "app.kubernetes.io/part-of": "Calico", + "k8s-app": "my-service", + "app.kubernetes.io/component": "Manager.operator.tigera.io", }, }, Spec: corev1.ServiceSpec{ @@ -1216,7 +1252,13 @@ var _ = Describe("Component handler tests", func() { Expect(c.Get(ctx, client.ObjectKey{Name: "my-service"}, svcWithIP)).NotTo(HaveOccurred()) Expect(svcWithIP.Spec.ClusterIP).To(Equal("10.96.0.1")) Expect(svcWithIP.Labels).To(Equal(map[string]string{ - "old": "should-be-preserved", + "old": "should-be-preserved", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/name": "my-service", + "app.kubernetes.io/part-of": "Calico", + "k8s-app": "my-service", + "app.kubernetes.io/component": "Manager.operator.tigera.io", })) // Now pretend we're the new operator version, wanting to remove the cluster IP. @@ -1242,8 +1284,14 @@ var _ = Describe("Component handler tests", func() { Expect(c.Get(ctx, client.ObjectKey{Name: "my-service"}, svcNoIP)).NotTo(HaveOccurred()) Expect(svcNoIP.Spec.ClusterIP).To(Equal("None")) Expect(svcNoIP.Labels).To(Equal(map[string]string{ - "old": "should-be-preserved", - "new": "should-be-added", + "old": "should-be-preserved", + "new": "should-be-added", + "k8s-app": "my-service", + "app.kubernetes.io/name": "my-service", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/part-of": "Calico", + "app.kubernetes.io/component": "Manager.operator.tigera.io", })) // The fake client resets the resource version to 1 on create. @@ -1256,9 +1304,15 @@ var _ = Describe("Component handler tests", func() { Expect(err).NotTo(HaveOccurred()) Expect(c.Get(ctx, client.ObjectKey{Name: "my-service"}, svcNoIP)).NotTo(HaveOccurred()) Expect(svcNoIP.Labels).To(Equal(map[string]string{ - "old": "should-be-preserved", - "new": "should-be-added", - "newer": "should-be-added", + "old": "should-be-preserved", + "new": "should-be-added", + "newer": "should-be-added", + "k8s-app": "my-service", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/component": "Manager.operator.tigera.io", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/name": "my-service", + "app.kubernetes.io/part-of": "Calico", })) Expect(svcNoIP.ObjectMeta.ResourceVersion).To(Equal("2"), "Expected update to rev ResourceVersion") @@ -1726,8 +1780,12 @@ var _ = Describe("Component handler tests", func() { By("checking that the daemonset is created and labels are added") expectedLabels := map[string]string{ - "k8s-app": "test-daemonset", - "app.kubernetes.io/name": "test-daemonset", + "k8s-app": "test-daemonset", + "app.kubernetes.io/name": "test-daemonset", + "app.kubernetes.io/component": "Manager.operator.tigera.io", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/part-of": "Calico", } expectedSelector := metav1.LabelSelector{ MatchLabels: map[string]string{"k8s-app": "test-daemonset"}, @@ -1764,8 +1822,12 @@ var _ = Describe("Component handler tests", func() { Expect(err).To(BeNil()) expectedLabels := map[string]string{ - "k8s-app": "test-daemonset", - "app.kubernetes.io/name": "test-daemonset", + "k8s-app": "test-daemonset", + "app.kubernetes.io/name": "test-daemonset", + "app.kubernetes.io/component": "Manager.operator.tigera.io", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/part-of": "Calico", } expectedSelector := metav1.LabelSelector{ MatchLabels: map[string]string{"preset-key": "preset-value"}, @@ -1797,8 +1859,12 @@ var _ = Describe("Component handler tests", func() { Expect(err).To(BeNil()) expectedLabels := map[string]string{ - "k8s-app": "test-deployment", - "app.kubernetes.io/name": "test-deployment", + "k8s-app": "test-deployment", + "app.kubernetes.io/name": "test-deployment", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/part-of": "Calico", + "app.kubernetes.io/component": "Manager.operator.tigera.io", } expectedSelector := metav1.LabelSelector{ MatchLabels: map[string]string{"k8s-app": "test-deployment"}, @@ -1836,8 +1902,12 @@ var _ = Describe("Component handler tests", func() { Expect(err).To(BeNil()) expectedLabels := map[string]string{ - "k8s-app": "test-deployment", - "app.kubernetes.io/name": "test-deployment", + "k8s-app": "test-deployment", + "app.kubernetes.io/name": "test-deployment", + "app.kubernetes.io/component": "Manager.operator.tigera.io", + "app.kubernetes.io/instance": "tigera-secure", + "app.kubernetes.io/managed-by": "tigera-operator", + "app.kubernetes.io/part-of": "Calico", } expectedSelector := metav1.LabelSelector{ MatchLabels: map[string]string{"preset-key": "preset-value"}, @@ -1852,6 +1922,20 @@ var _ = Describe("Component handler tests", func() { Expect(d.Spec.Template.GetLabels()).To(Equal(expectedLabels)) Expect(*d.Spec.Selector).To(Equal(expectedSelector)) }) + DescribeTable("should sanitize common labels so that they pass regexp validation", func(in string) { + Expect(sanitizeLabel(in)).To(MatchRegexp(`(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?`)) + }, + Entry("Valid, should remain unchanged", "My-Test_String.123"), + Entry("Invalid start/end, should be trimmed", "__My-Test_String.123.."), + Entry("Invalid characters (spaces)", "String with spaces"), + Entry("Invalid characters", "special-chars!@#$%^&*"), + Entry("Invalid start/end", "-leading-and-trailing-"), + Entry("Invalid start/end (multiple)", "____-leading-and-trailing-____"), + Entry("Empty string, should remain empty", ""), + Entry("Invalid, should become empty", "."), + Entry("Valid single character", "a"), + Entry("Valid", "a-b_c.d"), + Entry("Valid", "1.2.3.4")) }) Context("services account updates should not result in removal of data", func() { It("preserves secrets and image pull secrets that were present before object updates", func() { diff --git a/test/pool_test.go b/test/pool_test.go index ddb38f7a18..c4d2992762 100644 --- a/test/pool_test.go +++ b/test/pool_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2024-2025 Tigera, Inc. All rights reserved. +// Copyright (c) 2024-2026 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import ( "strings" "time" + "github.com/elastic/cloud-on-k8s/v2/pkg/utils/maps" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -189,7 +190,7 @@ var _ = Describe("IPPool FV tests", func() { Expect(ipPools.Items[0].Spec.Disabled).To(Equal(false)) Expect(ipPools.Items[0].Spec.BlockSize).To(Equal(26)) Expect(ipPools.Items[0].Spec.NodeSelector).To(Equal("all()")) - Expect(ipPools.Items[0].Labels).To(HaveLen(1)) + Expect(ipPools.Items[0].Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", "tigera-operator")) Expect(ipPools.Items[0].Spec.AssignmentMode).To(Equal(operator.AssignmentModeAutomatic)) }) @@ -251,7 +252,7 @@ var _ = Describe("IPPool FV tests", func() { Expect(len(ipPools.Items)).To(Equal(1), fmt.Sprintf("Expected 1 IP pool, but got: %+v", ipPools.Items)) // This proves the operator has not assumed control. - Expect(ipPools.Items[0].Labels).To(HaveLen(0)) + Expect(ipPools.Items[0].Labels).NotTo(HaveKey("app.kubernetes.io/managed-by")) // Now, install the API server. createAPIServer(c, mgr, shutdownContext, nil) @@ -268,14 +269,12 @@ var _ = Describe("IPPool FV tests", func() { if len(v3Pools.Items) != 1 { return fmt.Errorf("Expected 1 IP pool, but got: %+v", v3Pools.Items) } - if len(v3Pools.Items[0].Labels) != 1 { - return fmt.Errorf("Expected 1 label on IP pool, but got: %+v", v3Pools.Items[0].Labels) + if !maps.ContainsKeys(v3Pools.Items[0].Labels, "app.kubernetes.io/managed-by") { + return fmt.Errorf("Expected app.kubernetes.io/managed-by label, but got: %+v", v3Pools.Items[0].Labels) } return nil }, 5*time.Second, 1*time.Second).ShouldNot(HaveOccurred()) - Expect(v3Pools.Items[0].Labels).To(HaveKey("app.kubernetes.io/managed-by")) - // Verify that the default IPv4 pool has been subsumed by the operator. Expect(v3Pools.Items[0].Name).To(Equal("default-ipv4-ippool")) Expect(v3Pools.Items[0].Spec.CIDR).To(Equal("192.168.0.0/24")) @@ -351,7 +350,7 @@ var _ = Describe("IPPool FV tests", func() { Expect(len(ipPools.Items)).To(Equal(1), fmt.Sprintf("Expected 1 IP pool, but got: %+v", ipPools.Items)) // This proves the operator has not assumed control. - Expect(ipPools.Items[0].Labels).To(HaveLen(0)) + Expect(ipPools.Items[0].Labels).NotTo(HaveKey("app.kubernetes.io/managed-by")) // Now, install the API server. createAPIServer(c, mgr, shutdownContext, nil) From ad4ac1f27416a8c096372e0db9ed6c1d9d83f65b Mon Sep 17 00:00:00 2001 From: Rene Dekker Date: Fri, 30 Jan 2026 15:22:38 -0800 Subject: [PATCH 2/2] feat(recommended labels): Set recommended labels Set recommended labels as per https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ --- pkg/controller/utils/component.go | 45 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/pkg/controller/utils/component.go b/pkg/controller/utils/component.go index 1923282d00..680d821f94 100644 --- a/pkg/controller/utils/component.go +++ b/pkg/controller/utils/component.go @@ -17,6 +17,7 @@ package utils import ( "context" "fmt" + "maps" "reflect" "regexp" "slices" @@ -1004,39 +1005,35 @@ func setStandardSelectorAndLabels(obj client.Object, customResource metav1.Objec case *monitoringv1.Prometheus: d := obj if d.Spec.PodMetadata == nil { - d.Spec.PodMetadata = &monitoringv1.EmbeddedObjectMetadata{} - } - for k, v := range d.Labels { - d.Spec.PodMetadata.Labels[k] = v + d.Spec.PodMetadata = &monitoringv1.EmbeddedObjectMetadata{Labels: make(map[string]string)} } + maps.Copy(d.Spec.PodMetadata.Labels, d.Labels) case *monitoringv1.Alertmanager: d := obj if d.Spec.PodMetadata == nil { - d.Spec.PodMetadata = &monitoringv1.EmbeddedObjectMetadata{} - } - for k, v := range d.Labels { - d.Spec.PodMetadata.Labels[k] = v + d.Spec.PodMetadata = &monitoringv1.EmbeddedObjectMetadata{Labels: make(map[string]string)} } + maps.Copy(d.Spec.PodMetadata.Labels, d.Labels) default: return } - - if podTemplate.Labels == nil { - podTemplate.Labels = make(map[string]string) - } - if podTemplate.Labels["k8s-app"] == "" { - podTemplate.Labels["k8s-app"] = name - } - if customResource != nil { - // We do not want to set these labels on objects without a CR. They are usually deliberately not getting an - // owner ref and are not controlled by our operator. - addNameLabel(podTemplate, obj.GetName()) - addInstanceLabel(podTemplate, customResource) - addComponentLabel(podTemplate, customResource) - addPartOfLabel(podTemplate) - addManagedByLabel(podTemplate) + if podTemplate != nil { + if podTemplate.Labels == nil { + podTemplate.Labels = make(map[string]string) + } + if podTemplate.Labels["k8s-app"] == "" { + podTemplate.Labels["k8s-app"] = name + } + if customResource != nil { + // We do not want to set these labels on objects without a CR. They are usually deliberately not getting an + // owner ref and are not controlled by our operator. + addNameLabel(podTemplate, obj.GetName()) + addInstanceLabel(podTemplate, customResource) + addComponentLabel(podTemplate, customResource) + addPartOfLabel(podTemplate) + addManagedByLabel(podTemplate) + } } - } // sanitizeLabel cleans an input string to conform to the validation for labels. A valid label must be an empty string