Skip to content

Commit 8e257f2

Browse files
Merge pull request prometheus-operator#8478 from simonpasquier/update-shard-retention
Update shard retention
2 parents 161c85f + ac30f37 commit 8e257f2

8 files changed

Lines changed: 453 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## UNRELEASED
2+
3+
* [FEATURE] Implement shard retention based on Prometheus data retention (it requires the `PrometheusShardRetentionPolicy` feature gate). #8478
4+
15
## 0.90.1 / 2026-03-25
26

37
* [BUGFIX] Fix Probe ignoring HTTP client settings in scrape configuration. #8461

pkg/k8s/k8s.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -309,13 +309,15 @@ func AddTypeInformationToObject(obj runtime.Object) error {
309309

310310
// mergeMetadata takes labels and annotations from the old resource and merges
311311
// them into the new resource. If a key is present in both resources, the new
312-
// resource wins. It also copies the ResourceVersion from the old resource to
313-
// the new resource to prevent update conflicts.
312+
// resource wins. All keys starting with the "operator.prometheus.io/" prefix
313+
// in the old resource are dropped before merging.
314+
// It also copies the ResourceVersion from the old resource to the new resource
315+
// to prevent update conflicts.
314316
func mergeMetadata(newObj *metav1.ObjectMeta, oldObj metav1.ObjectMeta) {
315317
newObj.ResourceVersion = oldObj.ResourceVersion
316318

317-
newObj.SetLabels(mergeMap(oldObj.Labels, maps.All(newObj.Labels)))
318-
newObj.SetAnnotations(mergeMap(oldObj.Annotations, maps.All(newObj.Annotations)))
319+
newObj.SetLabels(mergeMap(maps.Collect(excludeOperatorPrefixSeq(oldObj.Labels)), maps.All(newObj.Labels)))
320+
newObj.SetAnnotations(mergeMap(maps.Collect(excludeOperatorPrefixSeq(oldObj.Annotations)), maps.All(newObj.Annotations)))
319321
}
320322

321323
// copyKubectlAnnotations copies the kubectl's annotations into the object
@@ -324,12 +326,12 @@ func copyKubectlAnnotations(objMeta *metav1.ObjectMeta, annotations map[string]s
324326
objMeta.SetAnnotations(mergeMap(objMeta.Annotations, filterByPrefixSeq(annotations, "kubectl.kubernetes.io/")))
325327
}
326328

327-
// filterByPrefixSeq returns a iterator over m for all keys matching the
328-
// prefix.
329-
func filterByPrefixSeq(m map[string]string, prefix string) iter.Seq2[string, string] {
329+
// excludeOperatorPrefixSeq returns a iterator over m excluding all keys
330+
// which start by the reserved operator's prefix.
331+
func excludeOperatorPrefixSeq(m map[string]string) iter.Seq2[string, string] {
330332
return func(yield func(k, v string) bool) {
331333
for k, v := range m {
332-
if !strings.HasPrefix(k, prefix) {
334+
if strings.HasPrefix(k, "operator.prometheus.io/") {
333335
continue
334336
}
335337
if !yield(k, v) {
@@ -339,6 +341,20 @@ func filterByPrefixSeq(m map[string]string, prefix string) iter.Seq2[string, str
339341
}
340342
}
341343

344+
// filterByPrefixSeq returns a iterator over m for all keys matching the
345+
// prefix.
346+
func filterByPrefixSeq(m map[string]string, prefix string) iter.Seq2[string, string] {
347+
return func(yield func(k, v string) bool) {
348+
for k, v := range m {
349+
if strings.HasPrefix(k, prefix) {
350+
if !yield(k, v) {
351+
return
352+
}
353+
}
354+
}
355+
}
356+
}
357+
342358
// mergeMap returns a clone of m for which key-value pairs from seq have been added.
343359
func mergeMap(m map[string]string, seq iter.Seq2[string, string]) map[string]string {
344360
// Don't mutate the input maps.
@@ -348,6 +364,5 @@ func mergeMap(m map[string]string, seq iter.Seq2[string, string]) map[string]str
348364
}
349365

350366
maps.Insert(m, seq)
351-
352367
return m
353368
}

pkg/k8s/k8s_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func TestMergeMetadata_CreateOrUpdateSecret(t *testing.T) {
6161
},
6262
},
6363
{
64-
name: "overridden label amd annotation",
64+
name: "overridden label and annotation",
6565
expectedLabels: map[string]string{
6666
"app.kubernetes.io/name": "kube-state-metrics",
6767
},
@@ -75,6 +75,21 @@ func TestMergeMetadata_CreateOrUpdateSecret(t *testing.T) {
7575
"app.kubernetes.io/name": "overridden-value",
7676
},
7777
},
78+
{
79+
name: "labels and annotations with reserved prefix are dropped",
80+
expectedLabels: map[string]string{
81+
"app.kubernetes.io/name": "kube-state-metrics",
82+
},
83+
modifiedLabels: map[string]string{
84+
"operator.prometheus.io/foo": "some value",
85+
},
86+
expectedAnnotations: map[string]string{
87+
"app.kubernetes.io/name": "kube-state-metrics",
88+
},
89+
modifiedAnnotations: map[string]string{
90+
"operator.prometheus.io/bar": "some value",
91+
},
92+
},
7893
}
7994

8095
namespace := "ns-1"

pkg/k8s/statefulset.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func CreateStatefulSetOrPatchLabels(ctx context.Context, ssetClient clientappsv1
7272
func ForceUpdateStatefulSet(ctx context.Context, ssetClient clientappsv1.StatefulSetInterface, sset *appsv1.StatefulSet, onDeleteFunc func(string)) error {
7373
err := updateStatefulSet(ctx, ssetClient, sset)
7474
if err == nil {
75-
return err
75+
return nil
7676
}
7777

7878
// When trying to update immutable fields, the API server returns a 422 status code.

pkg/operator/resource_reconciler.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,9 @@ func (rr *ResourceReconciler) objectKey(obj any) (string, bool) {
371371
return k, true
372372
}
373373

374-
func (rr *ResourceReconciler) resolve(obj metav1.Object) metav1.Object {
374+
// FindOwner returns the resource owning the given object.
375+
// For example it can return the Prometheus resource owning a StatefulSet.
376+
func (rr *ResourceReconciler) FindOwner(obj metav1.Object) metav1.Object {
375377
for _, or := range obj.GetOwnerReferences() {
376378
if !ptr.Deref(or.Controller, false) {
377379
continue
@@ -513,7 +515,7 @@ func (rr *ResourceReconciler) OnDelete(obj any) {
513515
}
514516

515517
func (rr *ResourceReconciler) onStatefulSetAdd(ss *appsv1.StatefulSet) {
516-
obj := rr.resolve(ss)
518+
obj := rr.FindOwner(ss)
517519
if obj == nil {
518520
return
519521
}
@@ -525,7 +527,7 @@ func (rr *ResourceReconciler) onStatefulSetAdd(ss *appsv1.StatefulSet) {
525527
}
526528

527529
func (rr *ResourceReconciler) onDaemonSetAdd(ds *appsv1.DaemonSet) {
528-
obj := rr.resolve(ds)
530+
obj := rr.FindOwner(ds)
529531
if obj == nil {
530532
return
531533
}
@@ -546,7 +548,7 @@ func (rr *ResourceReconciler) onStatefulSetUpdate(old, cur *appsv1.StatefulSet)
546548
return
547549
}
548550

549-
obj := rr.resolve(cur)
551+
obj := rr.FindOwner(cur)
550552
if obj == nil {
551553
return
552554
}
@@ -576,7 +578,7 @@ func (rr *ResourceReconciler) onDaemonSetUpdate(old, cur *appsv1.DaemonSet) {
576578
return
577579
}
578580

579-
obj := rr.resolve(cur)
581+
obj := rr.FindOwner(cur)
580582
if obj == nil {
581583
return
582584
}
@@ -595,7 +597,7 @@ func (rr *ResourceReconciler) onDaemonSetUpdate(old, cur *appsv1.DaemonSet) {
595597
}
596598

597599
func (rr *ResourceReconciler) onStatefulSetDelete(ss *appsv1.StatefulSet) {
598-
obj := rr.resolve(ss)
600+
obj := rr.FindOwner(ss)
599601
if obj == nil {
600602
return
601603
}
@@ -607,7 +609,7 @@ func (rr *ResourceReconciler) onStatefulSetDelete(ss *appsv1.StatefulSet) {
607609
}
608610

609611
func (rr *ResourceReconciler) onDaemonSetDelete(ds *appsv1.DaemonSet) {
610-
obj := rr.resolve(ds)
612+
obj := rr.FindOwner(ds)
611613
if obj == nil {
612614
return
613615
}

pkg/prometheus/server/operator.go

Lines changed: 137 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package prometheus
1616

1717
import (
1818
"context"
19+
"encoding/json"
1920
"errors"
2021
"fmt"
2122
"log/slog"
@@ -25,12 +26,15 @@ import (
2526

2627
"github.com/mitchellh/hashstructure"
2728
"github.com/prometheus/client_golang/prometheus"
29+
"github.com/prometheus/common/model"
2830
appsv1 "k8s.io/api/apps/v1"
2931
corev1 "k8s.io/api/core/v1"
3032
apierrors "k8s.io/apimachinery/pkg/api/errors"
3133
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3234
"k8s.io/apimachinery/pkg/labels"
35+
"k8s.io/apimachinery/pkg/types"
3336
"k8s.io/apimachinery/pkg/util/intstr"
37+
"k8s.io/apimachinery/pkg/util/wait"
3438
"k8s.io/client-go/dynamic"
3539
"k8s.io/client-go/kubernetes"
3640
"k8s.io/client-go/metadata"
@@ -60,6 +64,9 @@ const (
6064

6165
unmanagedConfigurationReason = "ConfigurationUnmanaged"
6266
unmanagedConfigurationMessage = "the operator doesn't manage the Prometheus configuration secret because neither serviceMonitorSelector nor podMonitorSelector, nor probeSelector, nor scrapeConfigSelector is specified. Unmanaged Prometheus configuration is deprecated, use additionalScrapeConfigs or the ScrapeConfig Custom Resource Definition instead. Unmanaged Prometheus configuration can also be disabled from the operator's command-line (check './operator --help')."
67+
68+
deletionDeadlineAnnotation = "operator.prometheus.io/deletion-deadline"
69+
annotationTimeFormat = time.RFC3339
6370
)
6471

6572
// Operator manages the life cycle of Prometheus deployments and
@@ -623,6 +630,45 @@ func (c *Operator) Run(ctx context.Context) error {
623630
// TODO(simonpasquier): watch for Prometheus pods instead of polling.
624631
go operator.StatusPoller(ctx, c)
625632

633+
if c.retentionPoliciesEnabled {
634+
// Periodically scan statefulsets for expired shards.
635+
go func() {
636+
_ = wait.PollUntilContextCancel(ctx, time.Minute, true, func(context.Context) (bool, error) {
637+
_ = c.ssetInfs.ListAll(labels.Everything(), func(o any) {
638+
sset := o.(*appsv1.StatefulSet)
639+
640+
deadline, found := sset.Annotations[deletionDeadlineAnnotation]
641+
if !found {
642+
return
643+
}
644+
645+
logger := c.logger.With("statefulset", fmt.Sprintf("%s/%s", sset.Namespace, sset.Name))
646+
expired, err := deadlineExpired(deadline)
647+
if err != nil {
648+
logger.Warn("failed to parse deletion deadline annotation", "err", err)
649+
return
650+
}
651+
652+
if !expired {
653+
logger.Debug("deletion deadline not expired", "deadline", deadline)
654+
return
655+
}
656+
657+
logger.Info("deletion deadline expired", "deadline", deadline)
658+
owner := c.rr.FindOwner(sset)
659+
if owner == nil {
660+
logger.Warn("owner not found")
661+
return
662+
}
663+
664+
c.rr.EnqueueForReconciliation(owner)
665+
})
666+
667+
return false, nil
668+
})
669+
}()
670+
}
671+
626672
c.metrics.Ready().Set(1)
627673
<-ctx.Done()
628674
return nil
@@ -962,7 +1008,7 @@ func (c *Operator) sync(ctx context.Context, key string) (func(context.Context)
9621008

9631009
ssetClient := c.kclient.AppsV1().StatefulSets(p.Namespace)
9641010

965-
// Ensure we have a StatefulSet running Prometheus deployed and that StatefulSet names are created correctly.
1011+
// Reconcile all active statefulset shards.
9661012
expected := prompkg.ExpectedStatefulSetShardNames(p)
9671013
for shard, ssetName := range expected {
9681014
logger := logger.With("statefulset", ssetName, "shard", fmt.Sprintf("%d", shard))
@@ -1060,18 +1106,19 @@ func (c *Operator) sync(ctx context.Context, key string) (func(context.Context)
10601106
return
10611107
}
10621108

1063-
shouldRetain, retainErr := c.shouldRetain(p)
1064-
if retainErr != nil {
1065-
deleteErrs = append(deleteErrs, fmt.Errorf("failed to determine if StatefulSet %s should be retained: %w", s.GetName(), retainErr))
1109+
shouldDelete, err := c.processShardRetention(ctx, p, s)
1110+
if err != nil {
1111+
logger.Warn("failed to process shard retention, not deleting the shard", "err", err, "statefulset", fmt.Sprintf("%s/%s", s.Namespace, s.Name))
10661112
return
10671113
}
1068-
if shouldRetain {
1114+
1115+
if !shouldDelete {
10691116
return
10701117
}
10711118

1072-
if delErr := ssetClient.Delete(ctx, s.GetName(), metav1.DeleteOptions{PropagationPolicy: ptr.To(metav1.DeletePropagationForeground)}); delErr != nil {
1073-
if !apierrors.IsNotFound(delErr) {
1074-
deleteErrs = append(deleteErrs, fmt.Errorf("failed to delete StatefulSet %s: %w", s.GetName(), delErr))
1119+
if err := ssetClient.Delete(ctx, s.GetName(), metav1.DeleteOptions{PropagationPolicy: ptr.To(metav1.DeletePropagationForeground)}); err != nil {
1120+
if !apierrors.IsNotFound(err) {
1121+
deleteErrs = append(deleteErrs, fmt.Errorf("failed to delete StatefulSet %s: %w", s.GetName(), err))
10751122
}
10761123
}
10771124
})
@@ -1202,23 +1249,95 @@ func (c *Operator) configResStatusCleanup(ctx context.Context, p *monitoringv1.P
12021249
return nil
12031250
}
12041251

1205-
// As the ShardRetentionPolicy feature evolves, should retain will evolve accordingly.
1206-
// For now, shouldRetain just returns the appropriate boolean based on the retention type.
1207-
func (c *Operator) shouldRetain(p *monitoringv1.Prometheus) (bool, error) {
1252+
// processShardRetention returns true if the associated statefulset should be
1253+
// deleted.
1254+
func (c *Operator) processShardRetention(ctx context.Context, p *monitoringv1.Prometheus, sset *appsv1.StatefulSet) (bool, error) {
12081255
if !c.retentionPoliciesEnabled {
1209-
// Feature-gate is disabled, default behavior is always to delete.
1210-
return false, nil
1256+
// When the feature gate is disabled, the default behavior is always to delete.
1257+
return true, nil
12111258
}
1259+
12121260
if p.Spec.ShardRetentionPolicy == nil {
1213-
// ShardRetentionPolicy not configured, default behavior is to delete.
1214-
return false, nil
1261+
// ShardRetentionPolicy not configured, the default behavior is to delete.
1262+
return true, nil
12151263
}
1216-
if ptr.Deref(p.Spec.ShardRetentionPolicy.WhenScaled,
1217-
monitoringv1.DeleteWhenScaledRetentionType) == monitoringv1.RetainWhenScaledRetentionType {
1264+
1265+
if ptr.Deref(p.Spec.ShardRetentionPolicy.WhenScaled, monitoringv1.DeleteWhenScaledRetentionType) != monitoringv1.RetainWhenScaledRetentionType {
12181266
return true, nil
12191267
}
12201268

1221-
return false, nil
1269+
if deadline, found := sset.Annotations[deletionDeadlineAnnotation]; found {
1270+
expired, err := deadlineExpired(deadline)
1271+
if err != nil {
1272+
return false, err
1273+
}
1274+
1275+
return expired, nil
1276+
}
1277+
1278+
// Set the annotation on the statefulset. If the deadline never expires,
1279+
// the annotation is set to the zero time value.
1280+
gracePeriod, err := gracePeriodForPrometheusStorage(p)
1281+
if err != nil {
1282+
return false, err
1283+
}
1284+
1285+
var deadline time.Time
1286+
if gracePeriod > 0 {
1287+
deadline = time.Now().UTC().Add(gracePeriod)
1288+
}
1289+
1290+
patchData, err := json.Marshal(map[string]any{
1291+
"metadata": map[string]any{
1292+
"annotations": map[string]string{
1293+
deletionDeadlineAnnotation: deadline.Format(annotationTimeFormat),
1294+
},
1295+
},
1296+
})
1297+
if err != nil {
1298+
return false, err
1299+
}
1300+
1301+
_, err = c.kclient.AppsV1().StatefulSets(sset.Namespace).Patch(
1302+
ctx,
1303+
sset.Name,
1304+
types.StrategicMergePatchType,
1305+
patchData,
1306+
metav1.PatchOptions{FieldManager: k8s.PrometheusOperatorFieldManager})
1307+
1308+
return false, err
1309+
}
1310+
1311+
func deadlineExpired(deadline string) (bool, error) {
1312+
t, err := time.Parse(annotationTimeFormat, deadline)
1313+
if err != nil {
1314+
return false, err
1315+
}
1316+
1317+
// A zero time means that the deadline never expires.
1318+
return !t.IsZero() && time.Now().UTC().After(t), nil
1319+
}
1320+
1321+
// gracePeriodForPrometheusStorage returns how long the Prometheus data can be available based
1322+
// on the retention settings.
1323+
// If Prometheus is configured with size-based retention only, it returns a
1324+
// zero value.
1325+
func gracePeriodForPrometheusStorage(p *monitoringv1.Prometheus) (time.Duration, error) {
1326+
if p.Spec.RetentionSize != "" && p.Spec.Retention == "" {
1327+
return time.Duration(0), nil
1328+
}
1329+
1330+
retention := p.Spec.Retention
1331+
if retention == "" {
1332+
retention = defaultRetention
1333+
}
1334+
1335+
d, err := model.ParseDuration(string(retention))
1336+
if err != nil {
1337+
return time.Duration(0), err
1338+
}
1339+
1340+
return time.Duration(d), nil
12221341
}
12231342

12241343
// UpdateStatus updates the status subresource of the object identified by the given

0 commit comments

Comments
 (0)