mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-19 14:34:17 +00:00
Run the E2E test on kind / setup-test-matrix (push) Failing after 3s
e2e-test-kind.yaml / extract (push) Failing after 5s
Run the E2E test on kind / get-go-version (push) Failing after 6s
Run the E2E test on kind / build (push) Skipped
Run the E2E test on kind / run-e2e-test (push) Skipped
push.yml / extract (push) Failing after 5s
Main CI / get-go-version (push) Failing after 6s
Main CI / Build (push) Skipped
Scorecard supply-chain security / Scorecard analysis (push) Skipped
* Fix backup queue permanently stuck when a dequeued backup completes during the patch Motivation: backupQueueReconciler patched a dequeued backup to ReadyToStart and only called backupTracker.AddReadyToStart on the next line. If backupReconciler picked up that patch and completed the backup (e.g. immediate FailedValidation while a BackupStorageLocation is briefly unavailable) before the queue controller reached that line, backupReconciler's Add + deferred Delete ran first, and the later AddReadyToStart re-inserted a tracker key nothing would ever delete again. backupTracker is in-memory and never reconciled against actual Backup phases, so RunningCount() stayed stuck at concurrentBackups and every later reconcile, including the periodic recheck, was refused at that gate -- the queue stopped dequeuing permanently until the deployment restarted. Approach: record the backup as ReadyToStart in the tracker before patching it, and roll that back if the patch itself fails, so the tracker entry always exists before the backup can become visible to any other reconciler. Also folds in two related fixes: the concurrency-refusal log line is now Info instead of Debug so a stuck queue is visible at the default log level, and the queue-position renumbering loop's error log (which built a logrus.Entry via log.WithError(errors.Wrapf(...)) but never called a terminal method on it, so it never actually logged anything) now emits properly. Validation: go build ./pkg/controller/..., go vet ./pkg/controller/..., and gofmt -l on both changed files are all clean. golangci-lint run ./pkg/controller/... reports no findings. go mod tidy produces a zero diff to go.mod/go.sum, matching this repo's verify-modules check. Mirrored this repo's own hack/test.sh invocation for this package (-short -vet=... -skip TestAPIs) and it passes; TestAPIs is a separate envtest suite that needs a local kubebuilder etcd binary not installed on this machine and fails identically on an unmodified checkout, so it is a pre-existing environment gap, not a regression. Added TestBackupQueueReconcilerTrackerNotLeakedWhenBackupCompletesDuringPatch, which uses a controller-runtime fake client with a Patch interceptor to simulate a racing reconciler completing the backup right after the ReadyToStart patch lands; it fails (RunningCount leaks to 1) against the pre-fix ordering and passes (RunningCount returns to 0) against the fix. Report: https://github.com/velero-io/velero/issues/10519 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-sonnet-5 (via Claude Code) * Add changelog file for PR #10521 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> * Add test covering tracker rollback when ReadyToStart patch fails Addresses review comment: verify backupTracker.RunningCount() returns to 0 when the ReadyToStart patch itself errors, covering the Delete rollback path alongside the existing race-condition regression test. Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> --------- Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> Co-authored-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Co-authored-by: Tiger Kaovilai <tkaovila@redhat.com>
324 lines
15 KiB
Go
324 lines
15 KiB
Go
/*
|
|
Copyright the Velero contributors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
//metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
//"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
|
|
|
|
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
|
"github.com/vmware-tanzu/velero/pkg/builder"
|
|
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
|
)
|
|
|
|
func TestBackupQueueReconciler(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
velerov1api.AddToScheme(scheme)
|
|
|
|
tests := []struct {
|
|
name string
|
|
priorBackups []*velerov1api.Backup
|
|
namespaces []string
|
|
backup *velerov1api.Backup
|
|
concurrentBackups int
|
|
expectError bool
|
|
expectPhase velerov1api.BackupPhase
|
|
expectQueuePosition int
|
|
}{
|
|
{
|
|
name: "New Backup gets queued",
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 1,
|
|
},
|
|
{
|
|
name: "InProgress Backup is ignored",
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
|
expectPhase: velerov1api.BackupPhaseInProgress,
|
|
},
|
|
{
|
|
name: "Second New Backup gets queued with queuePosition 2",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).Result(),
|
|
},
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-12").Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 2,
|
|
},
|
|
{
|
|
name: "Queued Backup moves to ReadyToStart if no others are running",
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseQueued).Result(),
|
|
expectPhase: velerov1api.BackupPhaseReadyToStart,
|
|
},
|
|
{
|
|
name: "Queued Backup remains queued if no spaces available",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-12").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
|
},
|
|
concurrentBackups: 2,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 1,
|
|
},
|
|
{
|
|
name: "Queued Backup remains queued if no spaces available including ReadyToStart",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-12").Phase(velerov1api.BackupPhaseReadyToStart).Result(),
|
|
},
|
|
concurrentBackups: 2,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 1,
|
|
},
|
|
{
|
|
name: "Queued Backup remains queued if earlier runnable backup is also queued",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-12").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).Result(),
|
|
},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(2).Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 2,
|
|
},
|
|
{
|
|
name: "Queued Backup remains queued if in conflict with running backup",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).IncludedNamespaces("foo").Result(),
|
|
},
|
|
namespaces: []string{"foo"},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).IncludedNamespaces("foo").Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 1,
|
|
},
|
|
{
|
|
name: "Queued Backup remains queued if in conflict with ReadyToStart backup",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseReadyToStart).IncludedNamespaces("foo").Result(),
|
|
},
|
|
namespaces: []string{"foo"},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).IncludedNamespaces("foo").Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 1,
|
|
},
|
|
{
|
|
name: "Queued Backup remains queued if in conflict with earlier queued backup",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).IncludedNamespaces("foo").Result(),
|
|
},
|
|
namespaces: []string{"foo", "bar"},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(2).IncludedNamespaces("foo", "bar").Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 2,
|
|
},
|
|
{
|
|
name: "Queued Backup remains queued if earlier non-ns-conflict backup exists",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).IncludedNamespaces("bar").Result(),
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-12").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).IncludedNamespaces("foo").Result(),
|
|
},
|
|
namespaces: []string{"foo", "bar", "baz"},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(2).IncludedNamespaces("baz").Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 2,
|
|
},
|
|
{
|
|
name: "Running all-namespace backup conflicts with queued one-namespace backup ",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).IncludedNamespaces("*").Result(),
|
|
},
|
|
namespaces: []string{"foo", "bar"},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).IncludedNamespaces("foo").Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 1,
|
|
},
|
|
{
|
|
name: "Running one-namespace backup conflicts with queued all-namespace backup ",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).IncludedNamespaces("bar").Result(),
|
|
},
|
|
namespaces: []string{"foo", "bar"},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).IncludedNamespaces("*").Result(),
|
|
expectPhase: velerov1api.BackupPhaseQueued,
|
|
expectQueuePosition: 1,
|
|
},
|
|
{
|
|
name: "Queued Backup moves to ReadyToStart if running count < concurrentBackups",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-12").Phase(velerov1api.BackupPhaseInProgress).Result(),
|
|
},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).Result(),
|
|
expectPhase: velerov1api.BackupPhaseReadyToStart,
|
|
},
|
|
{
|
|
name: "Queued Backup moves to ReadyToStart if running count < concurrentBackups and no ns conflict found",
|
|
priorBackups: []*velerov1api.Backup{
|
|
builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseReadyToStart).IncludedNamespaces("foo").Result(),
|
|
},
|
|
namespaces: []string{"foo", "bar"},
|
|
concurrentBackups: 3,
|
|
backup: builder.ForBackup(velerov1api.DefaultNamespace, "backup-20").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).IncludedNamespaces("bar").Result(),
|
|
expectPhase: velerov1api.BackupPhaseReadyToStart,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if test.backup == nil {
|
|
return
|
|
}
|
|
|
|
backupTracker := NewBackupTracker()
|
|
initObjs := []runtime.Object{}
|
|
for _, priorBackup := range test.priorBackups {
|
|
initObjs = append(initObjs, priorBackup)
|
|
if priorBackup.Status.Phase == velerov1api.BackupPhaseReadyToStart {
|
|
backupTracker.AddReadyToStart(priorBackup.Namespace, priorBackup.Name)
|
|
} else if priorBackup.Status.Phase == velerov1api.BackupPhaseInProgress {
|
|
backupTracker.Add(priorBackup.Namespace, priorBackup.Name)
|
|
}
|
|
}
|
|
for _, ns := range test.namespaces {
|
|
initObjs = append(initObjs, builder.ForNamespace(ns).Result())
|
|
}
|
|
initObjs = append(initObjs, test.backup)
|
|
|
|
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, initObjs...)
|
|
logger := logrus.New()
|
|
log := logger.WithField("controller", "backup-queue-test")
|
|
r := NewBackupQueueReconciler(fakeClient, scheme, log, test.concurrentBackups, backupTracker)
|
|
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.backup.Namespace, Name: test.backup.Name}}
|
|
res, err := r.Reconcile(t.Context(), req)
|
|
gotErr := err != nil
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ctrl.Result{}, res)
|
|
assert.Equal(t, test.expectError, gotErr)
|
|
backupAfter := velerov1api.Backup{}
|
|
err = fakeClient.Get(t.Context(), types.NamespacedName{
|
|
Namespace: test.backup.Namespace,
|
|
Name: test.backup.Name,
|
|
}, &backupAfter)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, test.expectPhase, backupAfter.Status.Phase)
|
|
assert.Equal(t, test.expectQueuePosition, backupAfter.Status.QueuePosition)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBackupQueueReconcilerTrackerNotLeakedWhenBackupCompletesDuringPatch verifies the fix for
|
|
// https://github.com/velero-io/velero/issues/10519: if backupReconciler picks up the
|
|
// ReadyToStart phase change (e.g. because the backup fails validation immediately) and calls
|
|
// BackupTracker.Add/Delete before backupQueueReconciler reaches its own AddReadyToStart call,
|
|
// the queue controller must not re-insert a tracker entry nobody will ever delete.
|
|
func TestBackupQueueReconcilerTrackerNotLeakedWhenBackupCompletesDuringPatch(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
require.NoError(t, velerov1api.AddToScheme(scheme))
|
|
|
|
backup := builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).Result()
|
|
backupTracker := NewBackupTracker()
|
|
|
|
fakeClient := velerotest.NewFakeControllerRuntimeClientBuilder(t).
|
|
WithRuntimeObjects(backup).
|
|
WithInterceptorFuncs(interceptor.Funcs{
|
|
Patch: func(ctx context.Context, c ctrlClient.WithWatch, obj ctrlClient.Object, patch ctrlClient.Patch, opts ...ctrlClient.PatchOption) error {
|
|
if err := c.Patch(ctx, obj, patch, opts...); err != nil {
|
|
return err
|
|
}
|
|
// Simulate backupReconciler racing ahead of this goroutine: it sees
|
|
// the ReadyToStart patch land, runs, and immediately completes the
|
|
// backup (e.g. FailedValidation), calling Add then its deferred
|
|
// Delete before backupQueueReconciler's next statement executes.
|
|
if b, ok := obj.(*velerov1api.Backup); ok && b.Status.Phase == velerov1api.BackupPhaseReadyToStart {
|
|
backupTracker.Add(b.Namespace, b.Name)
|
|
backupTracker.Delete(b.Namespace, b.Name)
|
|
}
|
|
return nil
|
|
},
|
|
}).
|
|
Build()
|
|
|
|
logger := logrus.New()
|
|
log := logger.WithField("controller", "backup-queue-test")
|
|
r := NewBackupQueueReconciler(fakeClient, scheme, log, 1, backupTracker)
|
|
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: backup.Namespace, Name: backup.Name}}
|
|
_, err := r.Reconcile(t.Context(), req)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 0, backupTracker.RunningCount(),
|
|
"tracker entry must not be leaked when a racing reconcile completes the backup before AddReadyToStart runs")
|
|
}
|
|
|
|
// TestBackupQueueReconcilerTrackerRolledBackWhenPatchFails verifies that if the
|
|
// ReadyToStart patch itself fails, the AddReadyToStart call made just before it is
|
|
// rolled back via Delete, so a failed patch doesn't itself permanently consume a
|
|
// concurrency slot.
|
|
func TestBackupQueueReconcilerTrackerRolledBackWhenPatchFails(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
require.NoError(t, velerov1api.AddToScheme(scheme))
|
|
|
|
backup := builder.ForBackup(velerov1api.DefaultNamespace, "backup-11").Phase(velerov1api.BackupPhaseQueued).QueuePosition(1).Result()
|
|
backupTracker := NewBackupTracker()
|
|
|
|
patchErr := errors.New("simulated patch failure")
|
|
fakeClient := velerotest.NewFakeControllerRuntimeClientBuilder(t).
|
|
WithRuntimeObjects(backup).
|
|
WithInterceptorFuncs(interceptor.Funcs{
|
|
Patch: func(ctx context.Context, c ctrlClient.WithWatch, obj ctrlClient.Object, patch ctrlClient.Patch, opts ...ctrlClient.PatchOption) error {
|
|
if b, ok := obj.(*velerov1api.Backup); ok && b.Status.Phase == velerov1api.BackupPhaseReadyToStart {
|
|
return patchErr
|
|
}
|
|
return c.Patch(ctx, obj, patch, opts...)
|
|
},
|
|
}).
|
|
Build()
|
|
|
|
logger := logrus.New()
|
|
log := logger.WithField("controller", "backup-queue-test")
|
|
r := NewBackupQueueReconciler(fakeClient, scheme, log, 1, backupTracker)
|
|
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: backup.Namespace, Name: backup.Name}}
|
|
_, err := r.Reconcile(t.Context(), req)
|
|
require.Error(t, err)
|
|
|
|
assert.Equal(t, 0, backupTracker.RunningCount(),
|
|
"tracker entry must be rolled back when the ReadyToStart patch fails")
|
|
}
|