diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index d508f80ef..9fb5f6ebd 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -321,6 +321,7 @@ func testAllNSPrometheus(t *testing.T) { "PrometheusUTF8MetricsSupport": testPrometheusUTF8MetricsSupport, "PrometheusUTF8LabelSupport": testPrometheusUTF8LabelSupport, "StuckStatefulSetRollout": testStuckStatefulSetRollout, + "PromScaleUpWithoutLabels": testPromScaleUpWithoutLabels, } for name, f := range testFuncs { diff --git a/test/e2e/prometheus_test.go b/test/e2e/prometheus_test.go index eedc4624d..7ac9331ee 100644 --- a/test/e2e/prometheus_test.go +++ b/test/e2e/prometheus_test.go @@ -6111,3 +6111,33 @@ type prometheusAlertmanagerAPIResponse struct { Status string `json:"status"` Data *alertmanagerDiscovery `json:"data"` } + +func testPromScaleUpWithoutLabels(t *testing.T) { + t.Parallel() + ctx := context.Background() + testCtx := framework.NewTestCtx(t) + defer testCtx.Cleanup(t) + ns := framework.CreateNamespace(ctx, t, testCtx) + framework.SetupPrometheusRBAC(ctx, t, testCtx, ns) + + name := "test" + + // Create a Prometheus resource with 1 replica + p, err := framework.CreatePrometheusAndWaitUntilReady(ctx, ns, framework.MakeBasicPrometheus(ns, name, name, 1)) + require.NoError(t, err) + + // Remove all labels on the StatefulSet using Patch + stsName := fmt.Sprintf("prometheus-%s", name) + err = framework.RemoveAllLabelsFromStatefulSet(ctx, stsName, ns) + require.NoError(t, err) + + // Scale up the Prometheus resource to 2 replicas + _, err = framework.UpdatePrometheusReplicasAndWaitUntilReady(ctx, p.Name, ns, 2) + require.NoError(t, err) + + // Verify the StatefulSet now has labels again (restored by the operator) + stsClient := framework.KubeClient.AppsV1().StatefulSets(ns) + sts, err := stsClient.Get(ctx, stsName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotEmpty(t, sts.GetLabels(), "expected labels to be restored on the StatefulSet by the operator") +} diff --git a/test/framework/framework.go b/test/framework/framework.go index 72fefd295..453b68898 100644 --- a/test/framework/framework.go +++ b/test/framework/framework.go @@ -859,3 +859,18 @@ func (f *Framework) CreateOrUpdateAdmissionWebhookServer( return service, certBytes, nil } + +func removeLabelsPatch(labels ...string) ([]byte, error) { + type patch struct { + Op string `json:"op"` + Path string `json:"path"` + } + + var patches []patch + encoder := strings.NewReplacer("/", "~1", "~", "~0") + for _, label := range labels { + patches = append(patches, patch{Op: "remove", Path: "/metadata/labels/" + encoder.Replace(label)}) + } + + return json.Marshal(patches) +} diff --git a/test/framework/namespace.go b/test/framework/namespace.go index 9e810aa7c..69dcab661 100644 --- a/test/framework/namespace.go +++ b/test/framework/namespace.go @@ -16,7 +16,6 @@ package framework import ( "context" - "encoding/json" "maps" "testing" @@ -81,25 +80,7 @@ func (f *Framework) AddLabelsToNamespace(ctx context.Context, name string, addit } func (f *Framework) RemoveLabelsFromNamespace(ctx context.Context, name string, labels ...string) error { - ns, err := f.KubeClient.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return err - } - - if len(ns.Labels) == 0 { - return nil - } - - type patch struct { - Op string `json:"op"` - Path string `json:"path"` - } - - var patches []patch - for _, l := range labels { - patches = append(patches, patch{Op: "remove", Path: "/metadata/labels/" + l}) - } - b, err := json.Marshal(patches) + b, err := removeLabelsPatch(labels...) if err != nil { return err } diff --git a/test/framework/statefulset.go b/test/framework/statefulset.go new file mode 100644 index 000000000..c2c50d52e --- /dev/null +++ b/test/framework/statefulset.go @@ -0,0 +1,53 @@ +// Copyright 2026 The prometheus-operator Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package framework + +import ( + "context" + "fmt" + "maps" + "slices" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// RemoveAllLabelsFromStatefulSet removes all labels from a StatefulSet using JSON Patch. +func (f *Framework) RemoveAllLabelsFromStatefulSet(ctx context.Context, name, namespace string) error { + sts, err := f.KubeClient.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get StatefulSet: %w", err) + } + + if len(sts.Labels) == 0 { + return nil + } + + b, err := removeLabelsPatch(slices.Sorted(maps.Keys(sts.GetLabels()))...) + if err != nil { + return fmt.Errorf("failed to marshal patch: %w", err) + } + + updatedSts, err := f.KubeClient.AppsV1().StatefulSets(namespace).Patch(ctx, name, types.JSONPatchType, b, metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("failed to patch StatefulSet: %w", err) + } + + if len(updatedSts.Labels) != 0 { + return fmt.Errorf("expected all labels to be removed from StatefulSet, but got %d labels: %v", len(updatedSts.Labels), updatedSts.Labels) + } + + return nil +}