From bd7b2ed690bbd725e48661e70deaff03597aa86a Mon Sep 17 00:00:00 2001 From: AmirHossein HajiMohammadi Date: Sat, 18 Jul 2026 17:13:54 +0330 Subject: [PATCH 01/18] Trim plugin image entries during install Signed-off-by: AmirHossein HajiMohammadi --- pkg/install/deployment.go | 5 ++++- pkg/install/deployment_test.go | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/install/deployment.go b/pkg/install/deployment.go index 4ce4b5a4f..e9474f1fe 100644 --- a/pkg/install/deployment.go +++ b/pkg/install/deployment.go @@ -139,7 +139,10 @@ func WithPodVolumeOperationTimeout(val time.Duration) podTemplateOption { func WithPlugins(plugins []string) podTemplateOption { return func(c *podTemplateConfig) { - c.plugins = plugins + c.plugins = make([]string, 0, len(plugins)) + for _, plugin := range plugins { + c.plugins = append(c.plugins, strings.TrimSpace(plugin)) + } } } diff --git a/pkg/install/deployment_test.go b/pkg/install/deployment_test.go index 53b696f72..0cfcb65dd 100644 --- a/pkg/install/deployment_test.go +++ b/pkg/install/deployment_test.go @@ -60,6 +60,15 @@ func TestDeployment(t *testing.T) { assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2) assert.Equal(t, "--features=EnableCSI,foo,bar,baz", deploy.Spec.Template.Spec.Containers[0].Args[1]) + deploy = Deployment("velero", WithPlugins([]string{ + "harbor-repo.vmware.com/harbor-ci/velero/velero-plugin-for-aws:v1.2.0", + " \n vsphereveleroplugin/velero-plugin-for-vsphere:v1.1.1 ", + })) + assert.Len(t, deploy.Spec.Template.Spec.InitContainers, 2) + assert.Equal(t, "harbor-repo.vmware.com/harbor-ci/velero/velero-plugin-for-aws:v1.2.0", deploy.Spec.Template.Spec.InitContainers[0].Image) + assert.Equal(t, "vsphereveleroplugin/velero-plugin-for-vsphere:v1.1.1", deploy.Spec.Template.Spec.InitContainers[1].Image) + assert.Equal(t, "vsphereveleroplugin-velero-plugin-for-vsphere", deploy.Spec.Template.Spec.InitContainers[1].Name) + deploy = Deployment("velero", WithUploaderType("kopia")) assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2) assert.Equal(t, "--uploader-type=kopia", deploy.Spec.Template.Spec.Containers[0].Args[1]) From 10c238ab5ab624820efec86587b95274ec6eaea6 Mon Sep 17 00:00:00 2001 From: AmirHossein HajiMohammadi Date: Sat, 18 Jul 2026 17:14:43 +0330 Subject: [PATCH 02/18] Add changelog for plugin install spacing Signed-off-by: AmirHossein HajiMohammadi --- changelogs/unreleased/10035-HajimohammadiNet | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/10035-HajimohammadiNet diff --git a/changelogs/unreleased/10035-HajimohammadiNet b/changelogs/unreleased/10035-HajimohammadiNet new file mode 100644 index 000000000..2905938ab --- /dev/null +++ b/changelogs/unreleased/10035-HajimohammadiNet @@ -0,0 +1 @@ +Trim whitespace around plugin image entries during install. From e9a778b848fe697343fb3e4134dd3abadb30830e Mon Sep 17 00:00:00 2001 From: Adam Zhang Date: Tue, 21 Jul 2026 10:49:55 +0800 Subject: [PATCH 03/18] update backup filters example 14 update the excludeNames to match example 3 for better consistency. Signed-off-by: Adam Zhang --- site/content/docs/main/fine-grained-backup-filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/docs/main/fine-grained-backup-filters.md b/site/content/docs/main/fine-grained-backup-filters.md index c8e7da63b..01f5aef8a 100644 --- a/site/content/docs/main/fine-grained-backup-filters.md +++ b/site/content/docs/main/fine-grained-backup-filters.md @@ -595,7 +595,7 @@ namespacedFilterPolicies: resourceFilters: - kinds: [ConfigMap] names: ["app-*"] - excludedNames: ["*-tmp", "*-debug"] + excludedNames: ["*-tmp-*", "*-debug-*", "*-tmp", "*-debug"] - kinds: [Secret] labelSelector: workload: application From 7ecf06d190fc6c6a1d07bc72d770d4dd30997359 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Wed, 22 Jul 2026 10:14:18 -0400 Subject: [PATCH 04/18] Fix CI: make Bitnami MinIO Dockerfile SHA lookup resilient (#10049) curl piped straight into jq with no error check; a non-JSON or failed HTTP response (rate limit, transient API error) broke jq with an opaque parse error. Add --fail-with-body, retries, and validate the parsed SHA before continuing. Fixes #10048 Signed-off-by: Tiger Kaovilai --- .github/workflows/e2e-test-kind.yaml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index fc77cb4d3..42dcaa707 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -62,8 +62,28 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - DOCKERFILE_SHA=$(curl -s -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2026/debian-12/Dockerfile\&per_page=1 | jq -r '.[0].sha') - echo "dockerfile_sha=${DOCKERFILE_SHA}" >> $GITHUB_OUTPUT + set -euo pipefail + + url="https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2026/debian-12/Dockerfile&per_page=1" + + response="$(curl --fail-with-body -sS \ + --retry 5 \ + --retry-delay 2 \ + --retry-all-errors \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$url")" + + DOCKERFILE_SHA="$(echo "$response" | jq -r '.[0].sha // empty')" + + if [ -z "$DOCKERFILE_SHA" ]; then + echo "Failed to resolve Bitnami MinIO Dockerfile SHA from GitHub API response" + echo "$response" + exit 1 + fi + + echo "dockerfile_sha=${DOCKERFILE_SHA}" >> "$GITHUB_OUTPUT" - name: Cache MinIO Image uses: actions/cache@v4 id: minio-cache From 1968bf44ee90ec9fa6909e65ed5d7121308a8058 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 09:42:55 -0700 Subject: [PATCH 05/18] Add ResetBackupLastSuccessfulTimestamp to ServerMetrics Add a method to reset all backupLastSuccessfulTimestamp gauge values. This will be used by the backup controller's periodic resync to prune stale metrics for deleted schedules. Fixes #9239 Signed-off-by: Shubham Pampattiwar --- pkg/metrics/metrics.go | 7 +++++++ pkg/metrics/metrics_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 86d78028c..d54eb02b5 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -758,6 +758,13 @@ func (m *ServerMetrics) RegisterPodVolumeOpLatencyGauge(node, pvbName, opName, b } } +// ResetBackupLastSuccessfulTimestamp removes all schedule-level backupLastSuccessfulTimestamp values. +func (m *ServerMetrics) ResetBackupLastSuccessfulTimestamp() { + if g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec); ok { + g.Reset() + } +} + // SetBackupTarballSizeBytesGauge records the size, in bytes, of a backup tarball. func (m *ServerMetrics) SetBackupTarballSizeBytesGauge(backupSchedule string, size int64) { if g, ok := m.metrics[backupTarballSizeBytesGauge].(*prometheus.GaugeVec); ok { diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index a24f2bf33..07004f172 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -457,6 +457,38 @@ func getHistogramCount(t *testing.T, vec *prometheus.HistogramVec, scheduleLabel return 0 } +// TestResetBackupLastSuccessfulTimestamp verifies that ResetBackupLastSuccessfulTimestamp +// removes all schedule-level values from the backupLastSuccessfulTimestamp gauge. +func TestResetBackupLastSuccessfulTimestamp(t *testing.T) { + m := NewServerMetrics() + + now := time.Now() + m.SetBackupLastSuccessfulTimestamp("schedule-1", now) + m.SetBackupLastSuccessfulTimestamp("schedule-2", now.Add(-time.Hour)) + m.SetBackupLastSuccessfulTimestamp("", now.Add(-2*time.Hour)) + + // Verify all three entries exist + g := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec) + assert.Equal(t, 3, collectGaugeCount(t, g)) + + // Reset should remove all entries + m.ResetBackupLastSuccessfulTimestamp() + assert.Equal(t, 0, collectGaugeCount(t, g)) +} + +// collectGaugeCount returns the number of time series in a GaugeVec. +func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { + t.Helper() + ch := make(chan prometheus.Metric, 10) + g.Collect(ch) + close(ch) + count := 0 + for range ch { + count++ + } + return count +} + // TestRepoMaintenanceMetrics verifies that repo maintenance metrics are properly recorded. func TestRepoMaintenanceMetrics(t *testing.T) { tests := []struct { From 8cf03998ddfc643830195128d0c72785ed24cfd0 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 09:47:39 -0700 Subject: [PATCH 06/18] Reset backupLastSuccessfulTimestamp during periodic resync The periodic backup metrics resync in updateTotalBackupMetric only set backupLastSuccessfulTimestamp values but never removed stale entries. When a schedule was deleted and its backups removed, the gauge persisted until the Velero pod was restarted. Reset the gauge before re-setting current values so that deleted schedules are pruned automatically each resync cycle. Fixes #9239 Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller.go | 4 ++- pkg/controller/backup_controller_test.go | 32 ++++++++++++++++++++++++ pkg/metrics/metrics.go | 17 +++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 74b857fd2..7a58424eb 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -215,7 +215,9 @@ func (b *backupReconciler) updateTotalBackupMetric() { } // recompute backup_last_successful_timestamp metric for each - // schedule (including the empty schedule, i.e. ad-hoc backups) + // schedule (including the empty schedule, i.e. ad-hoc backups). + // Reset first to prune stale entries for deleted schedules. + b.metrics.ResetBackupLastSuccessfulTimestamp() for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) { b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) } diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index bab98efb6..3a1903e0f 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -2041,6 +2041,38 @@ func Test_getLastSuccessBySchedule(t *testing.T) { } } +// Test_updateTotalBackupMetric_prunesStaleTimestamps verifies that the periodic +// resync removes backupLastSuccessfulTimestamp entries for schedules that no longer +// have any completed backups (e.g. after the schedule and its backups are deleted). +func Test_updateTotalBackupMetric_prunesStaleTimestamps(t *testing.T) { + baseTime, err := time.Parse(time.RFC1123, time.RFC1123) + require.NoError(t, err) + + m := metrics.NewServerMetrics() + + // Simulate a previous resync that set the metric for "deleted-schedule" + m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) + require.Equal(t, 1, m.BackupLastSuccessfulTimestampCount()) + + // Current backups only contain entries for "active-schedule" + backups := []velerov1api.Backup{ + *builder.ForBackup("velero", "b1"). + ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")). + Phase(velerov1api.BackupPhaseCompleted). + CompletionTimestamp(baseTime). + Result(), + } + + // Replicate the resync logic: reset then set + m.ResetBackupLastSuccessfulTimestamp() + for schedule, timestamp := range getLastSuccessBySchedule(backups) { + m.SetBackupLastSuccessfulTimestamp(schedule, timestamp) + } + + // Only "active-schedule" should remain; "deleted-schedule" should be pruned + assert.Equal(t, 1, m.BackupLastSuccessfulTimestampCount()) +} + // Unit tests to make sure that the backup's status is updated correctly during reconcile. // To clear up confusion whether status can be updated with Patch alone without status writer and not kbClient.Status().Patch() func TestPatchResourceWorksWithStatus(t *testing.T) { diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d54eb02b5..d95867fd1 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -765,6 +765,23 @@ func (m *ServerMetrics) ResetBackupLastSuccessfulTimestamp() { } } +// BackupLastSuccessfulTimestampCount returns the number of active time series +// in the backupLastSuccessfulTimestamp gauge. +func (m *ServerMetrics) BackupLastSuccessfulTimestampCount() int { + g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec) + if !ok { + return 0 + } + ch := make(chan prometheus.Metric, 100) + g.Collect(ch) + close(ch) + count := 0 + for range ch { + count++ + } + return count +} + // SetBackupTarballSizeBytesGauge records the size, in bytes, of a backup tarball. func (m *ServerMetrics) SetBackupTarballSizeBytesGauge(backupSchedule string, size int64) { if g, ok := m.metrics[backupTarballSizeBytesGauge].(*prometheus.GaugeVec); ok { From 4357ad89767ca0f968156658617d6f4e81eb6e65 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 14 Jul 2026 09:48:49 -0700 Subject: [PATCH 07/18] Add changelog for PR #10000 Signed-off-by: Shubham Pampattiwar --- changelogs/unreleased/10000-shubham-pampattiwar | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/10000-shubham-pampattiwar diff --git a/changelogs/unreleased/10000-shubham-pampattiwar b/changelogs/unreleased/10000-shubham-pampattiwar new file mode 100644 index 000000000..4134b77e2 --- /dev/null +++ b/changelogs/unreleased/10000-shubham-pampattiwar @@ -0,0 +1 @@ +Fix stale backupLastSuccessfulTimestamp metric after schedule deletion From 6ea95548d69e41744467ed69c634ba7ba8f30ad5 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 20 Jul 2026 06:10:53 -0700 Subject: [PATCH 08/18] Remove BackupLastSuccessfulTimestampCount, use Metrics() in test Remove the exported method that was only used in tests. Use the existing Metrics() getter to access the gauge directly in the backup controller test instead. Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller_test.go | 18 ++++++++++++++++-- pkg/metrics/metrics.go | 17 ----------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 3a1903e0f..3aafa2c71 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -31,6 +31,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -2049,10 +2050,11 @@ func Test_updateTotalBackupMetric_prunesStaleTimestamps(t *testing.T) { require.NoError(t, err) m := metrics.NewServerMetrics() + gauge := m.Metrics()["backup_last_successful_timestamp"].(*prometheus.GaugeVec) // Simulate a previous resync that set the metric for "deleted-schedule" m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) - require.Equal(t, 1, m.BackupLastSuccessfulTimestampCount()) + require.Equal(t, 1, collectGaugeCount(t, gauge)) // Current backups only contain entries for "active-schedule" backups := []velerov1api.Backup{ @@ -2070,7 +2072,19 @@ func Test_updateTotalBackupMetric_prunesStaleTimestamps(t *testing.T) { } // Only "active-schedule" should remain; "deleted-schedule" should be pruned - assert.Equal(t, 1, m.BackupLastSuccessfulTimestampCount()) + assert.Equal(t, 1, collectGaugeCount(t, gauge)) +} + +func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { + t.Helper() + ch := make(chan prometheus.Metric, 10) + g.Collect(ch) + close(ch) + count := 0 + for range ch { + count++ + } + return count } // Unit tests to make sure that the backup's status is updated correctly during reconcile. diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d95867fd1..d54eb02b5 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -765,23 +765,6 @@ func (m *ServerMetrics) ResetBackupLastSuccessfulTimestamp() { } } -// BackupLastSuccessfulTimestampCount returns the number of active time series -// in the backupLastSuccessfulTimestamp gauge. -func (m *ServerMetrics) BackupLastSuccessfulTimestampCount() int { - g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec) - if !ok { - return 0 - } - ch := make(chan prometheus.Metric, 100) - g.Collect(ch) - close(ch) - count := 0 - for range ch { - count++ - } - return count -} - // SetBackupTarballSizeBytesGauge records the size, in bytes, of a backup tarball. func (m *ServerMetrics) SetBackupTarballSizeBytesGauge(backupSchedule string, size int64) { if g, ok := m.metrics[backupTarballSizeBytesGauge].(*prometheus.GaugeVec); ok { From bfeccba0a84958943aae709766c663f73950160d Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 20 Jul 2026 12:32:02 -0700 Subject: [PATCH 09/18] Move metric reset inside List success block Avoid clearing backupLastSuccessfulTimestamp on transient API errors. The reset and re-set now only run when the backup List call succeeds, so existing metric values remain stable across temporary failures. Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 7a58424eb..0e2fb1384 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -212,14 +212,14 @@ func (b *backupReconciler) updateTotalBackupMetric() { b.logger.Error(err, "Error computing backup_total metric") } else { b.metrics.SetBackupTotal(int64(len(backups.Items))) - } - // recompute backup_last_successful_timestamp metric for each - // schedule (including the empty schedule, i.e. ad-hoc backups). - // Reset first to prune stale entries for deleted schedules. - b.metrics.ResetBackupLastSuccessfulTimestamp() - for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) { - b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) + // recompute backup_last_successful_timestamp metric for each + // schedule (including the empty schedule, i.e. ad-hoc backups). + // Reset first to prune stale entries for deleted schedules. + b.metrics.ResetBackupLastSuccessfulTimestamp() + for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) { + b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) + } } }, backupResyncPeriod, From b9d9dcfc385583d98fec0f7c1722344168c25327 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Tue, 21 Jul 2026 10:13:12 -0700 Subject: [PATCH 10/18] Add integration test for updateTotalBackupMetric resync Add a test that exercises the actual updateTotalBackupMetric goroutine with a fake client to verify stale backupLastSuccessfulTimestamp entries are pruned during a real resync cycle. Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller_test.go | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 3aafa2c71..9f6bf1a28 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -18,6 +18,7 @@ package controller import ( "bytes" + "context" "fmt" "io" "reflect" @@ -2087,6 +2088,44 @@ func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { return count } +// Test_updateTotalBackupMetric_prunesStaleTimestamps_integration tests the actual +// updateTotalBackupMetric goroutine with a fake client to verify stale metrics are +// pruned during a real resync cycle. +func Test_updateTotalBackupMetric_prunesStaleTimestamps_integration(t *testing.T) { + baseTime, err := time.Parse(time.RFC1123, time.RFC1123) + require.NoError(t, err) + + m := metrics.NewServerMetrics() + gauge := m.Metrics()["backup_last_successful_timestamp"].(*prometheus.GaugeVec) + + activeBackup := builder.ForBackup("velero", "b1"). + ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")). + Phase(velerov1api.BackupPhaseCompleted). + CompletionTimestamp(baseTime). + Result() + + fakeClient := velerotest.NewFakeControllerRuntimeClient(t, activeBackup) + + m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) + require.Equal(t, 1, collectGaugeCount(t, gauge)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + c := &backupReconciler{ + ctx: ctx, + kbClient: fakeClient, + logger: logrus.StandardLogger(), + metrics: m, + } + + c.updateTotalBackupMetric() + time.Sleep(7 * time.Second) + cancel() + + assert.Equal(t, 1, collectGaugeCount(t, gauge)) +} + // Unit tests to make sure that the backup's status is updated correctly during reconcile. // To clear up confusion whether status can be updated with Patch alone without status writer and not kbClient.Status().Patch() func TestPatchResourceWorksWithStatus(t *testing.T) { From 4ea38216f5d1e09a400c30e3e5977ba0dfb76346 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 09:16:24 -0700 Subject: [PATCH 11/18] Address review feedback: use DeleteLabelValues, extract resync method Replace blanket Reset() with targeted DeleteLabelValues to avoid briefly wiping metrics for schedules that still exist. Track known schedules in a set and only delete stale entries on each resync. Extract the wait.Until closure into resyncBackupMetrics() so tests can call it directly without goroutine timing. Replace hand-rolled collectGaugeCount helper with testutil.CollectAndCount. Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller.go | 58 +++++++++------- pkg/controller/backup_controller_test.go | 84 ++++++------------------ pkg/metrics/metrics.go | 7 +- pkg/metrics/metrics_test.go | 21 +++--- 4 files changed, 74 insertions(+), 96 deletions(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 0e2fb1384..01a660dad 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -107,10 +107,11 @@ type backupReconciler struct { credentialFileStore credentials.FileStore maxConcurrentK8SConnections int defaultSnapshotMoveData bool - globalCRClient kbclient.Client - itemBlockWorkerCount int - concurrentBackups int - globalVolumePoliciesConfigMap string + globalCRClient kbclient.Client + itemBlockWorkerCount int + concurrentBackups int + globalVolumePoliciesConfigMap string + knownSchedulesWithSuccessfulBackup sets.Set[string] } func NewBackupReconciler( @@ -204,30 +205,43 @@ func (b *backupReconciler) updateTotalBackupMetric() { time.Sleep(5 * time.Second) wait.Until( - func() { - // recompute backup_total metric - backups := &velerov1api.BackupList{} - err := b.kbClient.List(context.Background(), backups, &kbclient.ListOptions{LabelSelector: labels.Everything()}) - if err != nil { - b.logger.Error(err, "Error computing backup_total metric") - } else { - b.metrics.SetBackupTotal(int64(len(backups.Items))) - - // recompute backup_last_successful_timestamp metric for each - // schedule (including the empty schedule, i.e. ad-hoc backups). - // Reset first to prune stale entries for deleted schedules. - b.metrics.ResetBackupLastSuccessfulTimestamp() - for schedule, timestamp := range getLastSuccessBySchedule(backups.Items) { - b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) - } - } - }, + b.resyncBackupMetrics, backupResyncPeriod, b.ctx.Done(), ) }() } +func (b *backupReconciler) resyncBackupMetrics() { + backups := &velerov1api.BackupList{} + err := b.kbClient.List(context.Background(), backups, &kbclient.ListOptions{LabelSelector: labels.Everything()}) + if err != nil { + b.logger.Error(err, "Error computing backup_total metric") + return + } + + b.metrics.SetBackupTotal(int64(len(backups.Items))) + + currentSchedules := getLastSuccessBySchedule(backups.Items) + for schedule, timestamp := range currentSchedules { + b.metrics.SetBackupLastSuccessfulTimestamp(schedule, timestamp) + } + + // Remove metrics for schedules that no longer have successful backups + if b.knownSchedulesWithSuccessfulBackup != nil { + for schedule := range b.knownSchedulesWithSuccessfulBackup { + if _, exists := currentSchedules[schedule]; !exists { + b.metrics.DeleteBackupLastSuccessfulTimestamp(schedule) + } + } + } + + b.knownSchedulesWithSuccessfulBackup = sets.New[string]() + for schedule := range currentSchedules { + b.knownSchedulesWithSuccessfulBackup.Insert(schedule) + } +} + // getLastSuccessBySchedule finds the most recent completed backup for each schedule // and returns a map of schedule name -> completion time of the most recent completed // backup. This map includes an entry for ad-hoc/non-scheduled backups, where the key diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 9f6bf1a28..b86434796 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -18,7 +18,6 @@ package controller import ( "bytes" - "context" "fmt" "io" "reflect" @@ -32,7 +31,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" - "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -2043,60 +2042,15 @@ func Test_getLastSuccessBySchedule(t *testing.T) { } } -// Test_updateTotalBackupMetric_prunesStaleTimestamps verifies that the periodic -// resync removes backupLastSuccessfulTimestamp entries for schedules that no longer -// have any completed backups (e.g. after the schedule and its backups are deleted). -func Test_updateTotalBackupMetric_prunesStaleTimestamps(t *testing.T) { +// Test_resyncBackupMetrics_prunesStaleTimestamps verifies that resyncBackupMetrics +// removes backupLastSuccessfulTimestamp entries for schedules that no longer have +// any completed backups (e.g. after the schedule and its backups are deleted). +func Test_resyncBackupMetrics_prunesStaleTimestamps(t *testing.T) { baseTime, err := time.Parse(time.RFC1123, time.RFC1123) require.NoError(t, err) m := metrics.NewServerMetrics() - gauge := m.Metrics()["backup_last_successful_timestamp"].(*prometheus.GaugeVec) - - // Simulate a previous resync that set the metric for "deleted-schedule" - m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) - require.Equal(t, 1, collectGaugeCount(t, gauge)) - - // Current backups only contain entries for "active-schedule" - backups := []velerov1api.Backup{ - *builder.ForBackup("velero", "b1"). - ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")). - Phase(velerov1api.BackupPhaseCompleted). - CompletionTimestamp(baseTime). - Result(), - } - - // Replicate the resync logic: reset then set - m.ResetBackupLastSuccessfulTimestamp() - for schedule, timestamp := range getLastSuccessBySchedule(backups) { - m.SetBackupLastSuccessfulTimestamp(schedule, timestamp) - } - - // Only "active-schedule" should remain; "deleted-schedule" should be pruned - assert.Equal(t, 1, collectGaugeCount(t, gauge)) -} - -func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { - t.Helper() - ch := make(chan prometheus.Metric, 10) - g.Collect(ch) - close(ch) - count := 0 - for range ch { - count++ - } - return count -} - -// Test_updateTotalBackupMetric_prunesStaleTimestamps_integration tests the actual -// updateTotalBackupMetric goroutine with a fake client to verify stale metrics are -// pruned during a real resync cycle. -func Test_updateTotalBackupMetric_prunesStaleTimestamps_integration(t *testing.T) { - baseTime, err := time.Parse(time.RFC1123, time.RFC1123) - require.NoError(t, err) - - m := metrics.NewServerMetrics() - gauge := m.Metrics()["backup_last_successful_timestamp"].(*prometheus.GaugeVec) + gauge := m.Metrics()["backup_last_successful_timestamp"] activeBackup := builder.ForBackup("velero", "b1"). ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "active-schedule")). @@ -2104,26 +2058,30 @@ func Test_updateTotalBackupMetric_prunesStaleTimestamps_integration(t *testing.T CompletionTimestamp(baseTime). Result() - fakeClient := velerotest.NewFakeControllerRuntimeClient(t, activeBackup) + deletedBackup := builder.ForBackup("velero", "b2"). + ObjectMeta(builder.WithLabels(velerov1api.ScheduleNameLabel, "deleted-schedule")). + Phase(velerov1api.BackupPhaseCompleted). + CompletionTimestamp(baseTime). + Result() - m.SetBackupLastSuccessfulTimestamp("deleted-schedule", baseTime) - require.Equal(t, 1, collectGaugeCount(t, gauge)) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + fakeClient := velerotest.NewFakeControllerRuntimeClient(t, activeBackup, deletedBackup) c := &backupReconciler{ - ctx: ctx, kbClient: fakeClient, logger: logrus.StandardLogger(), metrics: m, } - c.updateTotalBackupMetric() - time.Sleep(7 * time.Second) - cancel() + // First resync: sets metrics for both schedules + c.resyncBackupMetrics() + assert.Equal(t, 2, testutil.CollectAndCount(gauge)) - assert.Equal(t, 1, collectGaugeCount(t, gauge)) + // Simulate schedule deletion: remove the backup for "deleted-schedule" + require.NoError(t, fakeClient.Delete(t.Context(), deletedBackup)) + + // Second resync: prunes "deleted-schedule" metric, keeps "active-schedule" + c.resyncBackupMetrics() + assert.Equal(t, 1, testutil.CollectAndCount(gauge)) } // Unit tests to make sure that the backup's status is updated correctly during reconcile. diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d54eb02b5..4661eaec8 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -758,10 +758,11 @@ func (m *ServerMetrics) RegisterPodVolumeOpLatencyGauge(node, pvbName, opName, b } } -// ResetBackupLastSuccessfulTimestamp removes all schedule-level backupLastSuccessfulTimestamp values. -func (m *ServerMetrics) ResetBackupLastSuccessfulTimestamp() { +// DeleteBackupLastSuccessfulTimestamp removes the backupLastSuccessfulTimestamp +// metric for a single schedule. +func (m *ServerMetrics) DeleteBackupLastSuccessfulTimestamp(scheduleName string) { if g, ok := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec); ok { - g.Reset() + g.DeleteLabelValues(scheduleName) } } diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 07004f172..2f2135ad1 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -21,6 +21,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -457,9 +458,9 @@ func getHistogramCount(t *testing.T, vec *prometheus.HistogramVec, scheduleLabel return 0 } -// TestResetBackupLastSuccessfulTimestamp verifies that ResetBackupLastSuccessfulTimestamp -// removes all schedule-level values from the backupLastSuccessfulTimestamp gauge. -func TestResetBackupLastSuccessfulTimestamp(t *testing.T) { +// TestDeleteBackupLastSuccessfulTimestamp verifies that DeleteBackupLastSuccessfulTimestamp +// removes only the specified schedule's metric. +func TestDeleteBackupLastSuccessfulTimestamp(t *testing.T) { m := NewServerMetrics() now := time.Now() @@ -467,13 +468,17 @@ func TestResetBackupLastSuccessfulTimestamp(t *testing.T) { m.SetBackupLastSuccessfulTimestamp("schedule-2", now.Add(-time.Hour)) m.SetBackupLastSuccessfulTimestamp("", now.Add(-2*time.Hour)) - // Verify all three entries exist g := m.metrics[backupLastSuccessfulTimestamp].(*prometheus.GaugeVec) - assert.Equal(t, 3, collectGaugeCount(t, g)) + assert.Equal(t, 3, testutil.CollectAndCount(g)) - // Reset should remove all entries - m.ResetBackupLastSuccessfulTimestamp() - assert.Equal(t, 0, collectGaugeCount(t, g)) + m.DeleteBackupLastSuccessfulTimestamp("schedule-1") + assert.Equal(t, 2, testutil.CollectAndCount(g)) + + m.DeleteBackupLastSuccessfulTimestamp("schedule-2") + assert.Equal(t, 1, testutil.CollectAndCount(g)) + + m.DeleteBackupLastSuccessfulTimestamp("") + assert.Equal(t, 0, testutil.CollectAndCount(g)) } // collectGaugeCount returns the number of time series in a GaugeVec. From 911cc9eb9e8ed5aadbb7a85c6acfd8ca2ff38bd9 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 09:57:28 -0700 Subject: [PATCH 12/18] Fix gofmt alignment in backupReconciler struct Signed-off-by: Shubham Pampattiwar --- pkg/controller/backup_controller.go | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 01a660dad..167fb7eaf 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -84,34 +84,34 @@ var autoExcludeClusterScopedResources = []string{ } type backupReconciler struct { - ctx context.Context - logger logrus.FieldLogger - discoveryHelper discovery.Helper - backupper pkgbackup.Backupper - kbClient kbclient.Client - clock clock.WithTickerAndDelayedExecution - backupLogLevel logrus.Level - newPluginManager func(logrus.FieldLogger) clientmgmt.Manager - backupTracker BackupTracker - defaultBackupLocation string - defaultVolumesToFsBackup bool - defaultBackupTTL time.Duration - defaultVGSLabelKey string - defaultCSISnapshotTimeout time.Duration - resourceTimeout time.Duration - defaultItemOperationTimeout time.Duration - defaultSnapshotLocations map[string]string - metrics *metrics.ServerMetrics - backupStoreGetter persistence.ObjectBackupStoreGetter - formatFlag logging.Format - credentialFileStore credentials.FileStore - maxConcurrentK8SConnections int - defaultSnapshotMoveData bool - globalCRClient kbclient.Client - itemBlockWorkerCount int - concurrentBackups int - globalVolumePoliciesConfigMap string - knownSchedulesWithSuccessfulBackup sets.Set[string] + ctx context.Context + logger logrus.FieldLogger + discoveryHelper discovery.Helper + backupper pkgbackup.Backupper + kbClient kbclient.Client + clock clock.WithTickerAndDelayedExecution + backupLogLevel logrus.Level + newPluginManager func(logrus.FieldLogger) clientmgmt.Manager + backupTracker BackupTracker + defaultBackupLocation string + defaultVolumesToFsBackup bool + defaultBackupTTL time.Duration + defaultVGSLabelKey string + defaultCSISnapshotTimeout time.Duration + resourceTimeout time.Duration + defaultItemOperationTimeout time.Duration + defaultSnapshotLocations map[string]string + metrics *metrics.ServerMetrics + backupStoreGetter persistence.ObjectBackupStoreGetter + formatFlag logging.Format + credentialFileStore credentials.FileStore + maxConcurrentK8SConnections int + defaultSnapshotMoveData bool + globalCRClient kbclient.Client + itemBlockWorkerCount int + concurrentBackups int + globalVolumePoliciesConfigMap string + knownSchedulesWithSuccessfulBackup sets.Set[string] } func NewBackupReconciler( From 0f01b534c314dbdd2378df5dfff93c7e3888f8d2 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 10:43:13 -0700 Subject: [PATCH 13/18] Remove unused collectGaugeCount, assert surviving label values Remove the unused collectGaugeCount helper and assert specific label values survive after each deletion using testutil.ToFloat64. Signed-off-by: Shubham Pampattiwar --- pkg/metrics/metrics_test.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 2f2135ad1..d7f070298 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -473,27 +473,17 @@ func TestDeleteBackupLastSuccessfulTimestamp(t *testing.T) { m.DeleteBackupLastSuccessfulTimestamp("schedule-1") assert.Equal(t, 2, testutil.CollectAndCount(g)) + assert.Equal(t, float64(now.Add(-time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues("schedule-2"))) + assert.Equal(t, float64(now.Add(-2*time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues(""))) m.DeleteBackupLastSuccessfulTimestamp("schedule-2") assert.Equal(t, 1, testutil.CollectAndCount(g)) + assert.Equal(t, float64(now.Add(-2*time.Hour).Unix()), testutil.ToFloat64(g.WithLabelValues(""))) m.DeleteBackupLastSuccessfulTimestamp("") assert.Equal(t, 0, testutil.CollectAndCount(g)) } -// collectGaugeCount returns the number of time series in a GaugeVec. -func collectGaugeCount(t *testing.T, g *prometheus.GaugeVec) int { - t.Helper() - ch := make(chan prometheus.Metric, 10) - g.Collect(ch) - close(ch) - count := 0 - for range ch { - count++ - } - return count -} - // TestRepoMaintenanceMetrics verifies that repo maintenance metrics are properly recorded. func TestRepoMaintenanceMetrics(t *testing.T) { tests := []struct { From e2249c26d5edda46ae1bfae77e4895126ab3a8c0 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 22 Jul 2026 10:56:39 -0700 Subject: [PATCH 14/18] Update docs and governance links from vmware-tanzu to velero-io The Velero repositories have moved to the velero-io GitHub organization. Update references in docs, GOVERNANCE.md, and SECURITY.md for repos that have migrated (velero, plugin-for-aws, plugin-for-gcp, plugin-for-microsoft-azure, plugin-for-example). References to repos that have not moved (helm-charts, plugin-for-vsphere, plugin-for-csi) are left unchanged. Signed-off-by: Shubham Pampattiwar --- GOVERNANCE.md | 20 +++++++++---------- SECURITY.md | 6 +++--- .../docs/main/csi-snapshot-data-movement.md | 2 +- site/content/docs/main/csi.md | 2 +- .../docs/main/fine-grained-backup-filters.md | 2 +- .../docs/main/plugin-release-instructions.md | 4 ++-- site/content/docs/main/support-process.md | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 687baeb00..73d5a7069 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -8,16 +8,16 @@ This document defines the project governance for Velero. ## Code Repositories -The following code repositories are governed by Velero community and maintained under the `vmware-tanzu\Velero` organization. +The following code repositories are governed by Velero community and maintained under the `velero-io` organization. -* **[Velero](https://github.com/vmware-tanzu/velero):** Main Velero codebase +* **[Velero](https://github.com/velero-io/velero):** Main Velero codebase * **[Helm Chart](https://github.com/vmware-tanzu/helm-charts/tree/main/charts/velero):** The Helm chart for the Velero server component * **[Velero CSI Plugin](https://github.com/vmware-tanzu/velero-plugin-for-csi):** This repository contains Velero plugins for snapshotting CSI backed PVCs using the CSI beta snapshot APIs * **[Velero Plugin for vSphere](https://github.com/vmware-tanzu/velero-plugin-for-vsphere):** This repository contains the Velero Plugin for vSphere. This plugin is a volume snapshotter plugin that provides crash-consistent snapshots of vSphere block volumes and backup of volume data into S3 compatible storage. -* **[Velero Plugin for AWS](https://github.com/vmware-tanzu/velero-plugin-for-aws):** This repository contains the plugins to support running Velero on AWS, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin for GCP](https://github.com/vmware-tanzu/velero-plugin-for-gcp):** This repository contains the plugins to support running Velero on GCP, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin for Azure](https://github.com/vmware-tanzu/velero-plugin-for-microsoft-azure):** This repository contains the plugins to support running Velero on Azure, including the object store plugin and the volume snapshotter plugin -* **[Velero Plugin Example](https://github.com/vmware-tanzu/velero-plugin-example):** This repository contains example plugins for Velero +* **[Velero Plugin for AWS](https://github.com/velero-io/velero-plugin-for-aws):** This repository contains the plugins to support running Velero on AWS, including the object store plugin and the volume snapshotter plugin +* **[Velero Plugin for GCP](https://github.com/velero-io/velero-plugin-for-gcp):** This repository contains the plugins to support running Velero on GCP, including the object store plugin and the volume snapshotter plugin +* **[Velero Plugin for Azure](https://github.com/velero-io/velero-plugin-for-microsoft-azure):** This repository contains the plugins to support running Velero on Azure, including the object store plugin and the volume snapshotter plugin +* **[Velero Plugin Example](https://github.com/velero-io/velero-plugin-example):** This repository contains example plugins for Velero ## Community Roles @@ -67,12 +67,12 @@ interested in implementing the proposal should be either deeply engaged in the proposal process or be an author of the proposal. The proposal should be documented as a separated markdown file pushed to the root of the -`design` folder in the [Velero](https://github.com/vmware-tanzu/velero/tree/main/design) +`design` folder in the [Velero](https://github.com/velero-io/velero/tree/main/design) repository via PR. The name of the file should follow the name pattern `_design.md`, e.g: `restore-hooks-design.md`. -Use the [Proposal Template](https://github.com/vmware-tanzu/velero/blob/main/design/_template.md) as a starting point. +Use the [Proposal Template](https://github.com/velero-io/velero/blob/main/design/_template.md) as a starting point. ### Proposal Lifecycle @@ -88,7 +88,7 @@ To maintain velocity in a project as busy as Velero, the concept of [Lazy Consensus](http://en.osswiki.info/concepts/lazy_consensus) is practiced. Ideas and / or proposals should be shared by maintainers via GitHub with the appropriate maintainer groups (e.g., -`@vmware-tanzu/velero-maintainers`) tagged. Out of respect for other contributors, +`@velero-io/velero-maintainers`) tagged. Out of respect for other contributors, major changes should also be accompanied by a ping on Slack or a note on the Velero mailing list as appropriate. Author(s) of proposal, Pull Requests, issues, etc. will give a time period of no less than five (5) working days for @@ -111,7 +111,7 @@ Lazy consensus does _not_ apply to the process of: ### Deprecation Process -Any contributor may introduce a request to deprecate a feature or an option of a feature by opening a feature request issue in the vmware-tanzu/velero GitHub project. The issue should describe why the feature is no longer needed or has become detrimental to Velero, as well as whether and how it has been superseded. The submitter should give as much detail as possible. +Any contributor may introduce a request to deprecate a feature or an option of a feature by opening a feature request issue in the velero-io/velero GitHub project. The issue should describe why the feature is no longer needed or has become detrimental to Velero, as well as whether and how it has been superseded. The submitter should give as much detail as possible. Once the issue is filed, a one-month discussion period begins. Discussions take place within the issue itself as well as in the community meetings. The person who opens the issue, or a maintainer, should add the date and time marking the end of the discussion period in a comment on the issue as soon as possible after it is opened. A decision on the issue needs to be made within this one-month period. diff --git a/SECURITY.md b/SECURITY.md index 84e6f45dc..219426f6f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ Velero is an open source tool with a growing community devoted to safe backup an ## Supported Versions -The Velero project maintains the following [governance document](https://github.com/vmware-tanzu/velero/blob/main/GOVERNANCE.md), [release document](https://github.com/vmware-tanzu/velero/blob/f42c63af1b9af445e38f78a7256b1c48ef79c10e/site/docs/main/release-instructions.md), and [support document](https://velero.io/docs/main/support-process/). Please refer to these for release and related details. Only the most recent version of Velero is supported. Each [release](https://github.com/vmware-tanzu/velero/releases) includes information about upgrading to the latest version. +The Velero project maintains the following [governance document](https://github.com/velero-io/velero/blob/main/GOVERNANCE.md), [release document](https://github.com/velero-io/velero/blob/f42c63af1b9af445e38f78a7256b1c48ef79c10e/site/docs/main/release-instructions.md), and [support document](https://velero.io/docs/main/support-process/). Please refer to these for release and related details. Only the most recent version of Velero is supported. Each [release](https://github.com/velero-io/velero/releases) includes information about upgrading to the latest version. ## Reporting a Vulnerability - Private Disclosure Process @@ -18,7 +18,7 @@ If you know of a publicly disclosed security vulnerability for Velero, please ** **IMPORTANT: Do not file public issues on GitHub for security vulnerabilities** -To report a vulnerability or a security-related issue, please contact the email address with the details of the vulnerability. The email will be fielded by the Security Team and then shared with the Velero maintainers who have committer and release permissions. Emails will be addressed within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/vmware-tanzu/velero/issues/new/choose) instead. +To report a vulnerability or a security-related issue, please contact the email address with the details of the vulnerability. The email will be fielded by the Security Team and then shared with the Velero maintainers who have committer and release permissions. Emails will be addressed within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/velero-io/velero/issues/new/choose) instead. ## Proposed Email Content @@ -68,7 +68,7 @@ The Security Team will respond to vulnerability reports as follows: ## Public Disclosure Process -The Security Team publishes a [public advisory](https://github.com/vmware-tanzu/velero/security/advisories) to the Velero community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog and other channels will assist in educating Velero users and rolling out the patched release to affected users. +The Security Team publishes a [public advisory](https://github.com/velero-io/velero/security/advisories) to the Velero community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog and other channels will assist in educating Velero users and rolling out the patched release to affected users. The Security Team will also publish any mitigating steps users can take until the fix can be applied to their Velero instances. Velero distributors will handle creating and publishing their own security advisories. diff --git a/site/content/docs/main/csi-snapshot-data-movement.md b/site/content/docs/main/csi-snapshot-data-movement.md index 154abb198..378f99055 100644 --- a/site/content/docs/main/csi-snapshot-data-movement.md +++ b/site/content/docs/main/csi-snapshot-data-movement.md @@ -67,7 +67,7 @@ On source cluster, Velero needs to manipulate CSI snapshots through the CSI volu To integrate Velero with the CSI volume snapshot APIs, you must enable the `EnableCSI` feature flag. -From release-1.14, the `github.com/vmware-tanzu/velero-plugin-for-csi` repository, which is the Velero CSI plugin, is merged into the `github.com/velero-io/velero` repository. +From release-1.14, the `github.com/velero-io/velero-plugin-for-csi` repository, which is the Velero CSI plugin, is merged into the `github.com/velero-io/velero` repository. The reasons to merge the CSI plugin are: * The VolumeSnapshot data mover depends on the CSI plugin, it's reasonabe to integrate them. * This change reduces the Velero deploying complexity. diff --git a/site/content/docs/main/csi.md b/site/content/docs/main/csi.md index fddc5f258..11973f50a 100644 --- a/site/content/docs/main/csi.md +++ b/site/content/docs/main/csi.md @@ -8,7 +8,7 @@ Integrating Container Storage Interface (CSI) snapshot support into Velero enabl By supporting CSI snapshot APIs, Velero can support any volume provider that has a CSI driver, without requiring a Velero-specific plugin to be available. This page gives an overview of how to add support for CSI snapshots to Velero. ## Notice -From release-1.14, the `github.com/vmware-tanzu/velero-plugin-for-csi` repository, which is the Velero CSI plugin, is merged into the `github.com/velero-io/velero` repository. +From release-1.14, the `github.com/velero-io/velero-plugin-for-csi` repository, which is the Velero CSI plugin, is merged into the `github.com/velero-io/velero` repository. The reasons to merge the CSI plugin are: * The VolumeSnapshot data mover depends on the CSI plugin, it's reasonabe to integrate them. * This change reduces the Velero deploying complexity. diff --git a/site/content/docs/main/fine-grained-backup-filters.md b/site/content/docs/main/fine-grained-backup-filters.md index 01f5aef8a..d9f90debd 100644 --- a/site/content/docs/main/fine-grained-backup-filters.md +++ b/site/content/docs/main/fine-grained-backup-filters.md @@ -778,7 +778,7 @@ Velero validates the ResourcePolicy when a backup starts. Common errors: Restore is unchanged: it restores whatever is in the backup archive. Resources excluded by fine-grained filters are simply absent. Use `Restore.spec.includedNamespaces` (and existing restore filters) to limit what you restore from a partial backup. -Fine-grained resource filtering is also available on the restore path using `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. For details on the restore-side policies, see the [Fine-grained restore filters design](https://github.com/vmware-tanzu/velero/blob/main/design/restore-filter-enhancement/fine-grained-restore-filters-design.md). +Fine-grained resource filtering is also available on the restore path using `namespacedFilterPolicies` and `clusterScopedFilterPolicy`. For details on the restore-side policies, see the [Fine-grained restore filters design](https://github.com/velero-io/velero/blob/main/design/restore-filter-enhancement/fine-grained-restore-filters-design.md). --- diff --git a/site/content/docs/main/plugin-release-instructions.md b/site/content/docs/main/plugin-release-instructions.md index 46494cac9..02ca6940d 100644 --- a/site/content/docs/main/plugin-release-instructions.md +++ b/site/content/docs/main/plugin-release-instructions.md @@ -19,11 +19,11 @@ Plugins the Velero core team is responsible include all those listed in [the Vel 1. Once the PR is merged, checkout the upstream `main` branch. Your local upstream might be named `upstream` or `origin`, so use this command: `git checkout /main`. 1. Tag the git version - `git tag v`. 1. Push the git tag - `git push --tags ` to trigger the image build. -2. Wait for the container images to build. You may check the progress of the GH action that triggers the image build at `https://github.com/vmware-tanzu//actions` +2. Wait for the container images to build. You may check the progress of the GH action that triggers the image build at `https://github.com/velero-io//actions` 3. Verify that an image with the new tag is available at `https://hub.docker.com/repository/docker/velero//`. 4. Run the Velero [e2e tests][2] using the new image. Until it is made configurable, you will have to edit the [plugin version][1] in the test. ### Release -1. If all e2e tests pass, go to the GitHub release page of the plugin (`https://github.com/vmware-tanzu//releases`) and manually create a release for the new tag. +1. If all e2e tests pass, go to the GitHub release page of the plugin (`https://github.com/velero-io//releases`) and manually create a release for the new tag. 1. Copy and paste the content of the new changelog file into the release description field. [1]: https://github.com/velero-io/velero/blob/c8dfd648bbe85db0184ea53296de4220895497e6/test/e2e/velero_utils.go#L27 diff --git a/site/content/docs/main/support-process.md b/site/content/docs/main/support-process.md index d142329f8..5c1363e7a 100644 --- a/site/content/docs/main/support-process.md +++ b/site/content/docs/main/support-process.md @@ -40,4 +40,4 @@ Generally speaking, new GitHub issues will fall into one of several categories. - If the issue ends up being a feature request or a bug, update the title and follow the appropriate process for it - If the reporter becomes unresponsive after multiple pings, close out the issue due to inactivity and comment that the user can always reach out again as needed -[0]: https://github.com/vmware-tanzu?q=velero&type=&language= +[0]: https://github.com/velero-io?q=velero&type=&language= From d69f6abe5cc31fbc5c0543db9ebce9f3e26a6c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wenkai=20Yin=28=E5=B0=B9=E6=96=87=E5=BC=80=29?= Date: Thu, 23 Jul 2026 10:31:42 +0800 Subject: [PATCH 15/18] Add param to StartRestore to facilitates future expansion (#10057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add param to StartRestore to facilitates future expansion Signed-off-by: Wenkai Yin(尹文开) --- pkg/controller/data_download_controller.go | 4 ++-- pkg/controller/data_download_controller_test.go | 4 ++-- pkg/controller/data_upload_controller_test.go | 2 +- pkg/controller/pod_volume_restore_controller.go | 4 ++-- pkg/controller/pod_volume_restore_controller_test.go | 4 ++-- pkg/datamover/restore_micro_service.go | 2 +- pkg/datamover/restore_micro_service_test.go | 4 ++-- pkg/datapath/data_path.go | 6 +++++- pkg/datapath/data_path_test.go | 2 +- pkg/datapath/micro_service_watcher.go | 2 +- pkg/datapath/mocks/asyncBR.go | 10 +++++----- pkg/datapath/types.go | 2 +- pkg/podvolume/restore_micro_service.go | 2 +- pkg/podvolume/restore_micro_service_test.go | 4 ++-- 14 files changed, 28 insertions(+), 24 deletions(-) diff --git a/pkg/controller/data_download_controller.go b/pkg/controller/data_download_controller.go index fc7cb1a53..7e06c459d 100644 --- a/pkg/controller/data_download_controller.go +++ b/pkg/controller/data_download_controller.go @@ -454,7 +454,7 @@ func (r *DataDownloadReconciler) startCancelableDataPath(asyncBR datapath.AsyncB if err := asyncBR.StartRestore(dd.Spec.SnapshotID, datapath.AccessPoint{ ByPath: res.ByPod.VolumeName, - }, dd.Spec.DataMoverConfig); err != nil { + }, dd.Spec.DataMoverConfig, nil); err != nil { return errors.Wrapf(err, "error starting async restore for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName) } @@ -1096,7 +1096,7 @@ func (r *DataDownloadReconciler) resumeCancellableDataPath(ctx context.Context, if err := asyncBR.StartRestore(dd.Spec.SnapshotID, datapath.AccessPoint{ ByPath: res.ByPod.VolumeName, - }, nil); err != nil { + }, nil, nil); err != nil { return errors.Wrapf(err, "error to resume asyncBR watcher for dd %s", dd.Name) } diff --git a/pkg/controller/data_download_controller_test.go b/pkg/controller/data_download_controller_test.go index 518788635..a605fcaaa 100644 --- a/pkg/controller/data_download_controller_test.go +++ b/pkg/controller/data_download_controller_test.go @@ -529,7 +529,7 @@ func TestDataDownloadReconcile(t *testing.T) { } if test.mockStart { - asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr) + asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr) } if test.mockCancel { @@ -1288,7 +1288,7 @@ func TestResumeCancellableRestore(t *testing.T) { } if test.mockStart { - mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr) + mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr) } if test.mockClose { diff --git a/pkg/controller/data_upload_controller_test.go b/pkg/controller/data_upload_controller_test.go index 9703abe92..ec819f8eb 100644 --- a/pkg/controller/data_upload_controller_test.go +++ b/pkg/controller/data_upload_controller_test.go @@ -348,7 +348,7 @@ func (f *fakeFSBR) StartBackup(source datapath.AccessPoint, uploaderConfigs map[ return f.startErr } -func (f *fakeFSBR) StartRestore(snapshotID string, target datapath.AccessPoint, uploaderConfigs map[string]string) error { +func (f *fakeFSBR) StartRestore(snapshotID string, target datapath.AccessPoint, uploaderConfigs map[string]string, param any) error { return nil } diff --git a/pkg/controller/pod_volume_restore_controller.go b/pkg/controller/pod_volume_restore_controller.go index 12ba49d10..ca25b4f95 100644 --- a/pkg/controller/pod_volume_restore_controller.go +++ b/pkg/controller/pod_volume_restore_controller.go @@ -528,7 +528,7 @@ func (r *PodVolumeRestoreReconciler) startCancelableDataPath(asyncBR datapath.As if err := asyncBR.StartRestore(pvr.Spec.SnapshotID, datapath.AccessPoint{ ByPath: res.ByPod.VolumeName, - }, pvr.Spec.UploaderSettings); err != nil { + }, pvr.Spec.UploaderSettings, nil); err != nil { return errors.Wrapf(err, "error starting async restore for pod %s, volume %s", res.ByPod.HostingPod.Name, res.ByPod.VolumeName) } @@ -1146,7 +1146,7 @@ func (r *PodVolumeRestoreReconciler) resumeCancellableDataPath(ctx context.Conte if err := asyncBR.StartRestore(pvr.Spec.SnapshotID, datapath.AccessPoint{ ByPath: res.ByPod.VolumeName, - }, pvr.Spec.UploaderSettings); err != nil { + }, pvr.Spec.UploaderSettings, nil); err != nil { return errors.Wrapf(err, "error to resume asyncBR watcher for PVR %s", pvr.Name) } diff --git a/pkg/controller/pod_volume_restore_controller_test.go b/pkg/controller/pod_volume_restore_controller_test.go index 61d34fae3..abd2df206 100644 --- a/pkg/controller/pod_volume_restore_controller_test.go +++ b/pkg/controller/pod_volume_restore_controller_test.go @@ -1099,7 +1099,7 @@ func TestPodVolumeRestoreReconcile(t *testing.T) { } if test.mockStart { - asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr) + asyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.mockStartErr) } if test.mockCancel { @@ -1901,7 +1901,7 @@ func TestResumeCancellablePodVolumeRestore(t *testing.T) { } if test.mockStart { - mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr) + mockAsyncBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startWatcherErr) } if test.mockClose { diff --git a/pkg/datamover/restore_micro_service.go b/pkg/datamover/restore_micro_service.go index d918667f9..5880dfc91 100644 --- a/pkg/datamover/restore_micro_service.go +++ b/pkg/datamover/restore_micro_service.go @@ -180,7 +180,7 @@ func (r *RestoreMicroService) RunCancelableDataPath(ctx context.Context) (string } log.Info("fs init") - if err := dp.StartRestore(dd.Spec.SnapshotID, r.sourceTargetPath, dd.Spec.DataMoverConfig); err != nil { + if err := dp.StartRestore(dd.Spec.SnapshotID, r.sourceTargetPath, dd.Spec.DataMoverConfig, &datapath.RestoreStartParam{}); err != nil { return "", errors.Wrap(err, "error starting data path restore") } diff --git a/pkg/datamover/restore_micro_service_test.go b/pkg/datamover/restore_micro_service_test.go index 33e22eab3..39e055572 100644 --- a/pkg/datamover/restore_micro_service_test.go +++ b/pkg/datamover/restore_micro_service_test.go @@ -355,12 +355,12 @@ func TestRunCancelableRestore(t *testing.T) { if test.startErr != nil { fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) - fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) + fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) } if test.dataPathStarted { fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) - fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(nil) + fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) } return fsBR diff --git a/pkg/datapath/data_path.go b/pkg/datapath/data_path.go index 6cef1af26..6e36ce6af 100644 --- a/pkg/datapath/data_path.go +++ b/pkg/datapath/data_path.go @@ -59,6 +59,10 @@ type BackupStartParam struct { SnapshotID string } +// RestoreStartParam define the input param for restore start +type RestoreStartParam struct { +} + type generalDataPath struct { ctx context.Context cancel context.CancelFunc @@ -221,7 +225,7 @@ func (dp *generalDataPath) StartBackup(source AccessPoint, uploaderConfig map[st return nil } -func (dp *generalDataPath) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string) error { +func (dp *generalDataPath) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string, param any) error { if !dp.initialized { return errors.New("data path is not initialized") } diff --git a/pkg/datapath/data_path_test.go b/pkg/datapath/data_path_test.go index 65d7f9b65..58df5d4e8 100644 --- a/pkg/datapath/data_path_test.go +++ b/pkg/datapath/data_path_test.go @@ -190,7 +190,7 @@ func TestAsyncRestore(t *testing.T) { dp.initialized = true dp.callbacks = test.callbacks - err := dp.StartRestore(test.snapshot, AccessPoint{ByPath: test.path}, map[string]string{}) + err := dp.StartRestore(test.snapshot, AccessPoint{ByPath: test.path}, map[string]string{}, &RestoreStartParam{}) require.NoError(t, err) <-finish diff --git a/pkg/datapath/micro_service_watcher.go b/pkg/datapath/micro_service_watcher.go index 3e8ace651..67ec4c29d 100644 --- a/pkg/datapath/micro_service_watcher.go +++ b/pkg/datapath/micro_service_watcher.go @@ -221,7 +221,7 @@ func (ms *microServiceBRWatcher) StartBackup(source AccessPoint, uploaderConfig return nil } -func (ms *microServiceBRWatcher) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string) error { +func (ms *microServiceBRWatcher) StartRestore(snapshotID string, target AccessPoint, uploaderConfigs map[string]string, param any) error { ms.log.Infof("Start watching restore ms to target %s, from snapshot %s", target.ByPath, snapshotID) ms.startWatch() diff --git a/pkg/datapath/mocks/asyncBR.go b/pkg/datapath/mocks/asyncBR.go index ef87fde83..deec61dae 100644 --- a/pkg/datapath/mocks/asyncBR.go +++ b/pkg/datapath/mocks/asyncBR.go @@ -60,17 +60,17 @@ func (_m *AsyncBR) StartBackup(source datapath.AccessPoint, dataMoverConfig map[ return r0 } -// StartRestore provides a mock function with given fields: snapshotID, target, dataMoverConfig -func (_m *AsyncBR) StartRestore(snapshotID string, target datapath.AccessPoint, dataMoverConfig map[string]string) error { - ret := _m.Called(snapshotID, target, dataMoverConfig) +// StartRestore provides a mock function with given fields: snapshotID, target, dataMoverConfig, param +func (_m *AsyncBR) StartRestore(snapshotID string, target datapath.AccessPoint, dataMoverConfig map[string]string, param interface{}) error { + ret := _m.Called(snapshotID, target, dataMoverConfig, param) if len(ret) == 0 { panic("no return value specified for StartRestore") } var r0 error - if rf, ok := ret.Get(0).(func(string, datapath.AccessPoint, map[string]string) error); ok { - r0 = rf(snapshotID, target, dataMoverConfig) + if rf, ok := ret.Get(0).(func(string, datapath.AccessPoint, map[string]string, interface{}) error); ok { + r0 = rf(snapshotID, target, dataMoverConfig, param) } else { r0 = ret.Error(0) } diff --git a/pkg/datapath/types.go b/pkg/datapath/types.go index a9c2331a6..65a6be58f 100644 --- a/pkg/datapath/types.go +++ b/pkg/datapath/types.go @@ -66,7 +66,7 @@ type AsyncBR interface { StartBackup(source AccessPoint, dataMoverConfig map[string]string, param any) error // StartRestore starts an asynchronous data path instance for restore - StartRestore(snapshotID string, target AccessPoint, dataMoverConfig map[string]string) error + StartRestore(snapshotID string, target AccessPoint, dataMoverConfig map[string]string, param any) error // Cancel cancels an asynchronous data path instance Cancel() diff --git a/pkg/podvolume/restore_micro_service.go b/pkg/podvolume/restore_micro_service.go index 24f001147..b9dbd8d64 100644 --- a/pkg/podvolume/restore_micro_service.go +++ b/pkg/podvolume/restore_micro_service.go @@ -184,7 +184,7 @@ func (r *RestoreMicroService) RunCancelableDataPath(ctx context.Context) (string log.Info("Async fs br init") - if err := fsRestore.StartRestore(pvr.Spec.SnapshotID, r.sourceTargetPath, pvr.Spec.UploaderSettings); err != nil { + if err := fsRestore.StartRestore(pvr.Spec.SnapshotID, r.sourceTargetPath, pvr.Spec.UploaderSettings, &datapath.RestoreStartParam{}); err != nil { return "", errors.Wrap(err, "error starting data path restore") } diff --git a/pkg/podvolume/restore_micro_service_test.go b/pkg/podvolume/restore_micro_service_test.go index 007060160..1964d5035 100644 --- a/pkg/podvolume/restore_micro_service_test.go +++ b/pkg/podvolume/restore_micro_service_test.go @@ -436,12 +436,12 @@ func TestRunCancelableDataPathRestore(t *testing.T) { if test.startErr != nil { fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) - fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) + fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(test.startErr) } if test.dataPathStarted { fsBR.On("Init", mock.Anything, mock.Anything).Return(nil) - fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything).Return(nil) + fsBR.On("StartRestore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) } return fsBR From e5654fa7eda520408896a2f1b09c806ab8c07012 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Thu, 23 Jul 2026 15:42:36 -0400 Subject: [PATCH 16/18] Fix flaky TestKopiaObjectWriterEx_ConcurrentAsyncErrors (#10030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test assumed all ten Write calls succeed before any async goroutine stores its error, but with a mock that fails instantly a goroutine can poison the writer mid-loop, making a later Write correctly fail fast — a timing-dependent test failure. Rewrite the test to assert the real-world contract instead of one schedule: a failed async block write either fails a subsequent Write fast or surfaces at Result, and is never lost. Add a separate deterministic case pinning the late-error schedule, holding async writes until all writes are queued so Result alone must report the error. Verified with -race -count=100. Fixes #10029 Signed-off-by: Tiger Kaovilai Co-authored-by: Claude Fable 5 --- .../udmrepo/kopialib/lib_repo_ex_test.go | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go index 3294063a6..6d698ec7b 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_ex_test.go @@ -1208,6 +1208,10 @@ func TestKopiaObjectWriterEx_MixedWriteAndWriteAt(t *testing.T) { assert.Equal(t, int64(3072), kow.entries[3].Start) } +// TestKopiaObjectWriterEx_ConcurrentAsyncErrors verifies the async error contract +// under real scheduling: once an async block write fails, the error either fails a +// subsequent Write call fast or surfaces at Result — it is never lost. Which of the +// two happens first depends on goroutine scheduling, and both are correct. func TestKopiaObjectWriterEx_ConcurrentAsyncErrors(t *testing.T) { mockRepoWriter := repomocks.NewMockRepositoryWriter(t) mockWriter := repomocks.NewWriter(t) @@ -1231,14 +1235,65 @@ func TestKopiaObjectWriterEx_ConcurrentAsyncErrors(t *testing.T) { data := make([]byte, 1024) - // Issue multiple writes so they all spawn async goroutines - // First few writes shouldn't fail immediately until getWriteError catches the asynchronous fault + // Issue multiple writes so they all spawn async goroutines. A later Write may + // observe the stored async error and fail fast — that is correct behavior. + for i := 0; i < 10; i++ { + l, err := kow.Write(data) + if err != nil { + assert.Contains(t, err.Error(), "simulated async error") + break + } + assert.Equal(t, 1024, l) + } + + // Regardless of whether a Write observed the error first, Result must report it. + id, err := kow.Result() + + require.Error(t, err) + assert.Contains(t, err.Error(), "simulated async error") + assert.Equal(t, udmrepo.ID(""), id) +} + +// TestKopiaObjectWriterEx_AsyncErrorSurfacesAtResult pins the late-error schedule: +// async writes are held until all writes have been queued, so no Write call observes +// the failure and Result alone must report it. +func TestKopiaObjectWriterEx_AsyncErrorSurfacesAtResult(t *testing.T) { + mockRepoWriter := repomocks.NewMockRepositoryWriter(t) + mockWriter := repomocks.NewWriter(t) + + releaseWrites := make(chan struct{}) + mockWriter.On("Write", mock.Anything).Run(func(mock.Arguments) { + <-releaseWrites + }).Return(0, errors.New("simulated async error")) + mockWriter.On("Close").Return(nil) + + mockRepoWriter.On("NewObjectWriter", mock.Anything, mock.Anything).Return(mockWriter) + + sem := make(chan struct{}, 10) + buf := freelist.New(10*1024, 1024) + + kow := &kopiaObjectWriterEx{ + ctx: context.Background(), + rawRepoWriter: mockRepoWriter, + blockSize: 1024, + asyncWritesSem: sem, + asyncBuffer: buf, + logger: velerotest.NewLogger(), + } + + data := make([]byte, 1024) + + // All async writes block on releaseWrites, so no error can be stored yet and + // every Write must succeed. for i := 0; i < 10; i++ { l, err := kow.Write(data) require.NoError(t, err) assert.Equal(t, 1024, l) } + close(releaseWrites) + + // Result waits for the async writers to finish and must report their error. id, err := kow.Result() require.Error(t, err) From 5ecf38b5d7fdcd081aa7b64f4777eab74e350de1 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Thu, 23 Jul 2026 15:43:47 -0400 Subject: [PATCH 17/18] Derive dev-tool CLI versions from go.mod (ginkgo, protoc-gen-go, goimports) (#10024) * Derive Ginkgo CLI version from go.mod in test/Makefile Hardcoded @v2.22.0 pin drifted from go.mod's v2.28.3, causing Ginkgo CLI/package version mismatch warnings. Fixes #10023 Signed-off-by: Tiger Kaovilai * Derive protoc-gen-go and goimports versions from go.mod in build-image Same drift issue as #10023: Dockerfile hardcoded @v1.33.0 and @v0.33.0 while go.mod had moved on. Build context is hack/build-image, which doesn't include go.mod, so versions are computed in the Makefile (which does have go.mod) and passed through as build-args, same as GOPROXY. protoc-gen-go-grpc and controller-gen/setup-envtest/golangci-lint are left as-is: no matching go.mod entry, or independently versioned from the module they live alongside. Fixes #10023 Signed-off-by: Tiger Kaovilai --------- Signed-off-by: Tiger Kaovilai --- Makefile | 9 +++++++-- hack/build-image/Dockerfile | 10 ++++++---- test/Makefile | 3 ++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 515abf88d..bb766c7c9 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,11 @@ GOARCH = $(word 2, $(platform_temp)) GOPROXY ?= https://proxy.golang.org GOBIN=$$(pwd)/.go/bin +# Keep these build-image tool versions in sync with go.mod so the CLI/library +# pair doesn't drift (see https://github.com/velero-io/velero/issues/10023). +PROTOC_GEN_GO_VERSION := $(shell go list -m -f '{{.Version}}' google.golang.org/protobuf) +GOIMPORTS_VERSION := $(shell go list -m -f '{{.Version}}' golang.org/x/tools) + # If you want to build all binaries, see the 'all-build' rule. # If you want to build all containers, see the 'all-containers' rule. all: @@ -395,9 +400,9 @@ ifeq ($(BUILDX_ENABLED), true) ifneq ($(CONTAINER_TOOL),docker) $(error $(DOCKER_ONLY_ERROR)) endif - @cd hack/build-image && $(CONTAINER_TOOL) buildx build --build-arg=GOPROXY=$(GOPROXY) --output=type=docker --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) . + @cd hack/build-image && $(CONTAINER_TOOL) buildx build --build-arg=GOPROXY=$(GOPROXY) --build-arg=PROTOC_GEN_GO_VERSION=$(PROTOC_GEN_GO_VERSION) --build-arg=GOIMPORTS_VERSION=$(GOIMPORTS_VERSION) --output=type=docker --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) . else - @cd hack/build-image && $(CONTAINER_TOOL) build --build-arg=GOPROXY=$(GOPROXY) --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) . + @cd hack/build-image && $(CONTAINER_TOOL) build --build-arg=GOPROXY=$(GOPROXY) --build-arg=PROTOC_GEN_GO_VERSION=$(PROTOC_GEN_GO_VERSION) --build-arg=GOIMPORTS_VERSION=$(GOIMPORTS_VERSION) --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) . endif $(eval new_id=$(shell $(CONTAINER_TOOL) image inspect --format '{{ .ID }}' ${BUILDER_IMAGE} 2>/dev/null)) @if [ "$(old_id)" != "" ] && [ "$(old_id)" != "$(new_id)" ]; then \ diff --git a/hack/build-image/Dockerfile b/hack/build-image/Dockerfile index 88dedde95..aa725da03 100644 --- a/hack/build-image/Dockerfile +++ b/hack/build-image/Dockerfile @@ -15,6 +15,8 @@ FROM --platform=$TARGETPLATFORM golang:1.26-trixie ARG GOPROXY +ARG PROTOC_GEN_GO_VERSION +ARG GOIMPORTS_VERSION ENV GO111MODULE=on # Use a proxy for go modules to reduce the likelihood of various hosts being down and breaking the build @@ -34,9 +36,9 @@ RUN wget --quiet https://github.com/kubernetes-sigs/kubebuilder/releases/downloa # get controller-tools RUN go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.5 -# get goimports (the revision is pinned so we don't indiscriminately update, but the particular commit -# is not important) -RUN go install golang.org/x/tools/cmd/goimports@v0.33.0 +# get goimports, version derived from go.mod's golang.org/x/tools requirement +# (see https://github.com/velero-io/velero/issues/10023) +RUN go install golang.org/x/tools/cmd/goimports@${GOIMPORTS_VERSION} # get protoc compiler and golang plugin WORKDIR /root @@ -71,7 +73,7 @@ RUN ARCH=$(go env GOARCH) && \ chmod a+x /usr/include/google/protobuf && \ chmod a+r -R /usr/include/google && \ chmod +x /usr/bin/protoc -RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.33.0 \ +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOC_GEN_GO_VERSION} \ && go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 # get goreleaser diff --git a/test/Makefile b/test/Makefile index ae58e2c95..4f051ae00 100644 --- a/test/Makefile +++ b/test/Makefile @@ -48,6 +48,7 @@ GOBIN := $(REPO_ROOT)/.go/bin TOOLS_BIN_DIR := $(TOOLS_DIR)/$(BIN_DIR) GINKGO := $(GOBIN)/ginkgo +GINKGO_VERSION := $(shell go list -m -f '{{.Version}}' github.com/onsi/ginkgo/v2 2>/dev/null) KUSTOMIZE := $(TOOLS_BIN_DIR)/kustomize @@ -186,7 +187,7 @@ ginkgo: ${GOBIN}/ginkgo # This target does not run if ginkgo is already in $GOBIN ${GOBIN}/ginkgo: - GOBIN=${GOBIN} go install github.com/onsi/ginkgo/v2/ginkgo@v2.22.0 + GOBIN=${GOBIN} go install github.com/onsi/ginkgo/v2/ginkgo@${GINKGO_VERSION} .PHONY: run-e2e run-e2e: ginkgo From 92f636ca528e98e70fcf8986a544b3c598f0678c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:52:44 -0400 Subject: [PATCH 18/18] Bump google.golang.org/grpc from 1.81.1 to 1.82.1 (#10058) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.1 to 1.82.1. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.81.1...v1.82.1) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.82.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index a2c41faf6..3aa6ea020 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( golang.org/x/sys v0.46.0 golang.org/x/text v0.37.0 google.golang.org/api v0.283.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af k8s.io/api v0.36.0 k8s.io/apiextensions-apiserver v0.36.0 @@ -76,7 +76,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect @@ -189,7 +189,7 @@ require ( github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect diff --git a/go.sum b/go.sum index ed0070272..63cf28c46 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMs github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= @@ -466,8 +466,8 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= -go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= @@ -564,8 +564,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=