From bcdee1b1165a2fea5bb9254467c4ef40afb862b1 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Mon, 9 Feb 2026 16:10:00 +0800 Subject: [PATCH 1/5] If BIA return updateObj with SkipFromBackupAnnotation, treat it as skip the resource from backup. Signed-off-by: Xun Jiang --- changelogs/unreleased/9547-blackpiglet | 1 + pkg/apis/velero/v1/labels_annotations.go | 9 +++++++++ pkg/backup/item_backupper.go | 13 +++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 changelogs/unreleased/9547-blackpiglet diff --git a/changelogs/unreleased/9547-blackpiglet b/changelogs/unreleased/9547-blackpiglet new file mode 100644 index 000000000..f1e546be6 --- /dev/null +++ b/changelogs/unreleased/9547-blackpiglet @@ -0,0 +1 @@ +If BIA return updateObj with SkipFromBackupAnnotation, treat it as skip the resource from backup. \ No newline at end of file diff --git a/pkg/apis/velero/v1/labels_annotations.go b/pkg/apis/velero/v1/labels_annotations.go index c1431d3cc..85d8b05aa 100644 --- a/pkg/apis/velero/v1/labels_annotations.go +++ b/pkg/apis/velero/v1/labels_annotations.go @@ -102,6 +102,15 @@ const ( // even if the resource contains a matching selector label. ExcludeFromBackupLabel = "velero.io/exclude-from-backup" + // SkipFromBackupAnnotation is the annotation used by internal BackupItemActions + // to indicate that a resource should be skipped from backup, + // even if it doesn't have the ExcludeFromBackupLabel. + // This is used in cases where we want to skip backup of a resource based on some logic in a plugin. + // + // Notice: SkipFromBackupAnnotation's priority is higher than MustIncludeAdditionalItemAnnotation. + // If SkipFromBackupAnnotation is set, the resource will be skipped even if MustIncludeAdditionalItemAnnotation is set. + SkipFromBackupAnnotation = "velero.io/skip-from-backup" + // defaultVGSLabelKey is the default label key used to group PVCs under a VolumeGroupSnapshot DefaultVGSLabelKey = "velero.io/volume-group" diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index feae0e01c..770b1ed41 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -244,6 +244,12 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti return false, itemFiles, kubeerrs.NewAggregate(backupErrs) } + // If err is nil and updatedObj is nil, it means the item is skipped by plugin action, + // we should return here to avoid backing up the item, and avoid potential NPE in the following code. + if updatedObj == nil { + return false, itemFiles, nil + } + itemFiles = append(itemFiles, additionalItemFiles...) obj = updatedObj if metadata, err = meta.Accessor(obj); err != nil { @@ -398,6 +404,13 @@ func (ib *itemBackupper) executeActions( } u := &unstructured.Unstructured{Object: updatedItem.UnstructuredContent()} + + if _, ok := u.GetAnnotations()[velerov1api.SkipFromBackupAnnotation]; ok { + log.Infof("Resource (groupResource=%s, namespace=%s, name=%s) is skipped from backup by action %s.", + groupResource.String(), namespace, name, actionName) + return nil, itemFiles, nil + } + if actionName == csiBIAPluginName { if additionalItemIdentifiers == nil && u.GetAnnotations()[velerov1api.SkippedNoCSIPVAnnotation] == "true" { // snapshot was skipped by CSI plugin From 3f15e9219f5e073d49d3685fe236e7830abfc130 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 27 Feb 2026 00:28:49 +0800 Subject: [PATCH 2/5] Remove the skipped item from the resource list when it's skipped by BIA. Signed-off-by: Xun Jiang --- pkg/backup/backed_up_items_map.go | 8 ++++++++ pkg/backup/item_backupper.go | 2 ++ 2 files changed, 10 insertions(+) diff --git a/pkg/backup/backed_up_items_map.go b/pkg/backup/backed_up_items_map.go index f5764cd8e..174a50e1e 100644 --- a/pkg/backup/backed_up_items_map.go +++ b/pkg/backup/backed_up_items_map.go @@ -98,6 +98,14 @@ func (m *backedUpItemsMap) AddItem(key itemKey) { m.totalItems[key] = struct{}{} } +func (m *backedUpItemsMap) DeleteItem(key itemKey) { + m.Lock() + defer m.Unlock() + + delete(m.backedUpItems, key) + delete(m.totalItems, key) +} + func (m *backedUpItemsMap) AddItemToTotal(key itemKey) { m.Lock() defer m.Unlock() diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index 770b1ed41..b50f4e119 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -247,6 +247,8 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti // If err is nil and updatedObj is nil, it means the item is skipped by plugin action, // we should return here to avoid backing up the item, and avoid potential NPE in the following code. if updatedObj == nil { + log.Infof("Remove item from the backup's backupItems list and totalItems list because it's skipped by plugin action.") + ib.backupRequest.BackedUpItems.DeleteItem(key) return false, itemFiles, nil } From 6c3d81a146ceda77d73150bc58e63a5c8329b0cf Mon Sep 17 00:00:00 2001 From: Quang Ngo Date: Mon, 2 Mar 2026 10:19:16 +1100 Subject: [PATCH 3/5] Add schedule_expected_interval_seconds metric Add a new Prometheus gauge metric that exposes the expected interval between consecutive scheduled backups. This enables dynamic alerting thresholds per schedule backups. Signed-off-by: Quang Ngo --- pkg/controller/schedule_controller.go | 7 +++ pkg/metrics/metrics.go | 22 +++++++ pkg/metrics/metrics_test.go | 84 +++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/pkg/controller/schedule_controller.go b/pkg/controller/schedule_controller.go index ec8894571..443b3c08b 100644 --- a/pkg/controller/schedule_controller.go +++ b/pkg/controller/schedule_controller.go @@ -129,6 +129,13 @@ func (c *scheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } else { schedule.Status.Phase = velerov1.SchedulePhaseEnabled schedule.Status.ValidationErrors = nil + + // Compute expected interval between consecutive scheduled backup runs. + // Only meaningful when the cron expression is valid. + now := c.clock.Now() + nextRun := cronSchedule.Next(now) + nextNextRun := cronSchedule.Next(nextRun) + c.metrics.SetScheduleExpectedIntervalSeconds(schedule.Name, nextNextRun.Sub(nextRun).Seconds()) } scheduleNeedsPatch := false diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 30e67a7b6..86d78028c 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -80,6 +80,9 @@ const ( DataDownloadFailureTotal = "data_download_failure_total" DataDownloadCancelTotal = "data_download_cancel_total" + // schedule metrics + scheduleExpectedIntervalSeconds = "schedule_expected_interval_seconds" + // repo maintenance metrics repoMaintenanceSuccessTotal = "repo_maintenance_success_total" repoMaintenanceFailureTotal = "repo_maintenance_failure_total" @@ -347,6 +350,14 @@ func NewServerMetrics() *ServerMetrics { }, []string{scheduleLabel, backupNameLabel}, ), + scheduleExpectedIntervalSeconds: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: metricNamespace, + Name: scheduleExpectedIntervalSeconds, + Help: "Expected interval between consecutive scheduled backups, in seconds", + }, + []string{scheduleLabel}, + ), repoMaintenanceSuccessTotal: prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: metricNamespace, @@ -644,6 +655,9 @@ func (m *ServerMetrics) RemoveSchedule(scheduleName string) { if c, ok := m.metrics[csiSnapshotFailureTotal].(*prometheus.CounterVec); ok { c.DeleteLabelValues(scheduleName, "") } + if g, ok := m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec); ok { + g.DeleteLabelValues(scheduleName) + } } // InitMetricsForNode initializes counter metrics for a node. @@ -758,6 +772,14 @@ func (m *ServerMetrics) SetBackupLastSuccessfulTimestamp(backupSchedule string, } } +// SetScheduleExpectedIntervalSeconds records the expected interval in seconds, +// between consecutive backups for a schedule. +func (m *ServerMetrics) SetScheduleExpectedIntervalSeconds(scheduleName string, seconds float64) { + if g, ok := m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec); ok { + g.WithLabelValues(scheduleName).Set(seconds) + } +} + // SetBackupTotal records the current number of existent backups. func (m *ServerMetrics) SetBackupTotal(numberOfBackups int64) { if g, ok := m.metrics[backupTotal].(prometheus.Gauge); ok { diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 184e496ab..a24f2bf33 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -259,6 +259,90 @@ func TestMultipleAdhocBackupsShareMetrics(t *testing.T) { assert.Equal(t, float64(1), validationFailureMetric, "All adhoc validation failures should be counted together") } +// TestSetScheduleExpectedIntervalSeconds verifies that the expected interval metric +// is properly recorded for schedules. +func TestSetScheduleExpectedIntervalSeconds(t *testing.T) { + tests := []struct { + name string + scheduleName string + intervalSeconds float64 + description string + }{ + { + name: "every 5 minutes schedule", + scheduleName: "frequent-backup", + intervalSeconds: 300, + description: "Expected interval should be 5m in seconds", + }, + { + name: "daily schedule", + scheduleName: "daily-backup", + intervalSeconds: 86400, + description: "Expected interval should be 24h in seconds", + }, + { + name: "monthly schedule", + scheduleName: "monthly-backup", + intervalSeconds: 2678400, // 31 days in seconds + description: "Expected interval should be 31 days in seconds", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + m := NewServerMetrics() + m.SetScheduleExpectedIntervalSeconds(tc.scheduleName, tc.intervalSeconds) + + metric := getMetricValue(t, m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec), tc.scheduleName) + assert.Equal(t, tc.intervalSeconds, metric, tc.description) + }) + } +} + +// TestScheduleExpectedIntervalNotInitializedByDefault verifies that the expected +// interval metric is not initialized by InitSchedule, so it only appears for +// schedules with a valid cron expression. +func TestScheduleExpectedIntervalNotInitializedByDefault(t *testing.T) { + m := NewServerMetrics() + m.InitSchedule("test-schedule") + + // The metric should not have any values after InitSchedule + ch := make(chan prometheus.Metric, 1) + m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec).Collect(ch) + close(ch) + + count := 0 + for range ch { + count++ + } + assert.Equal(t, 0, count, "scheduleExpectedIntervalSeconds should not be initialized by InitSchedule") +} + +// TestRemoveScheduleCleansUpExpectedInterval verifies that RemoveSchedule +// cleans up the expected interval metric. +func TestRemoveScheduleCleansUpExpectedInterval(t *testing.T) { + m := NewServerMetrics() + m.InitSchedule("test-schedule") + m.SetScheduleExpectedIntervalSeconds("test-schedule", 3600) + + // Verify metric exists + metric := getMetricValue(t, m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec), "test-schedule") + assert.Equal(t, float64(3600), metric) + + // Remove schedule and verify metric is cleaned up + m.RemoveSchedule("test-schedule") + + ch := make(chan prometheus.Metric, 1) + m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec).Collect(ch) + close(ch) + + count := 0 + for range ch { + count++ + } + assert.Equal(t, 0, count, "scheduleExpectedIntervalSeconds should be removed after RemoveSchedule") +} + // TestInitScheduleWithEmptyName verifies that InitSchedule works correctly // with an empty schedule name (for adhoc backups). func TestInitScheduleWithEmptyName(t *testing.T) { From 1c08af84614e7f1197d9fe5b9a117668851c1afd Mon Sep 17 00:00:00 2001 From: Quang Ngo Date: Mon, 2 Mar 2026 10:49:14 +1100 Subject: [PATCH 4/5] Add changelog for #9570 Signed-off-by: Quang Ngo --- changelogs/unreleased/9570-H-M-Quang-Ngo | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/9570-H-M-Quang-Ngo diff --git a/changelogs/unreleased/9570-H-M-Quang-Ngo b/changelogs/unreleased/9570-H-M-Quang-Ngo new file mode 100644 index 000000000..603cd75e5 --- /dev/null +++ b/changelogs/unreleased/9570-H-M-Quang-Ngo @@ -0,0 +1 @@ +Add schedule_expected_interval_seconds metric for dynamic backup alerting thresholds (#9559) From d1cc30355385093a1e8839662dda8ac93ce5d04c Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 9 Mar 2026 15:38:29 +0800 Subject: [PATCH 5/5] issue 9586: set latest doc to 1.18 Signed-off-by: Lyndon-Li --- pkg/util/podvolume/pod_volume_test.go | 2 +- site/config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/util/podvolume/pod_volume_test.go b/pkg/util/podvolume/pod_volume_test.go index a3484c2e3..87a6f3a0f 100644 --- a/pkg/util/podvolume/pod_volume_test.go +++ b/pkg/util/podvolume/pod_volume_test.go @@ -156,7 +156,7 @@ func TestGetVolumesByPod(t *testing.T) { Volumes: []corev1api.Volume{ // PVB Volumes {Name: "pvbPV1"}, {Name: "pvbPV2"}, {Name: "pvbPV3"}, - /// Excluded from PVB because colume mounting default service account token + /// Excluded from PVB because volume mounting default service account token {Name: "default-token-5xq45"}, }, }, diff --git a/site/config.yaml b/site/config.yaml index ed80914a4..8eded5b59 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -12,7 +12,7 @@ params: hero: backgroundColor: med-blue versioning: true - latest: v1.17 + latest: v1.18 versions: - main - v1.18