feat(backup): add global backup volume policies

Signed-off-by: chlins <chlins.zhang@gmail.com>
This commit is contained in:
chlins
2026-06-25 11:31:08 +08:00
parent 960a20f12c
commit 21d67a8622
14 changed files with 654 additions and 89 deletions
+60 -53
View File
@@ -84,32 +84,33 @@ 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
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
}
func NewBackupReconciler(
@@ -138,34 +139,36 @@ func NewBackupReconciler(
itemBlockWorkerCount int,
concurrentBackups int,
globalCRClient kbclient.Client,
globalVolumePoliciesConfigMap string,
) *backupReconciler {
b := &backupReconciler{
ctx: ctx,
discoveryHelper: discoveryHelper,
backupper: backupper,
clock: &clock.RealClock{},
logger: logger,
backupLogLevel: backupLogLevel,
newPluginManager: newPluginManager,
backupTracker: backupTracker,
kbClient: kbClient,
defaultBackupLocation: defaultBackupLocation,
defaultVolumesToFsBackup: defaultVolumesToFsBackup,
defaultBackupTTL: defaultBackupTTL,
defaultVGSLabelKey: defaultVGSLabelKey,
defaultCSISnapshotTimeout: defaultCSISnapshotTimeout,
resourceTimeout: resourceTimeout,
defaultItemOperationTimeout: defaultItemOperationTimeout,
defaultSnapshotLocations: defaultSnapshotLocations,
metrics: metrics,
backupStoreGetter: backupStoreGetter,
formatFlag: formatFlag,
credentialFileStore: credentialStore,
maxConcurrentK8SConnections: maxConcurrentK8SConnections,
defaultSnapshotMoveData: defaultSnapshotMoveData,
itemBlockWorkerCount: itemBlockWorkerCount,
concurrentBackups: max(concurrentBackups, 1),
globalCRClient: globalCRClient,
ctx: ctx,
discoveryHelper: discoveryHelper,
backupper: backupper,
clock: &clock.RealClock{},
logger: logger,
backupLogLevel: backupLogLevel,
newPluginManager: newPluginManager,
backupTracker: backupTracker,
kbClient: kbClient,
defaultBackupLocation: defaultBackupLocation,
defaultVolumesToFsBackup: defaultVolumesToFsBackup,
defaultBackupTTL: defaultBackupTTL,
defaultVGSLabelKey: defaultVGSLabelKey,
defaultCSISnapshotTimeout: defaultCSISnapshotTimeout,
resourceTimeout: resourceTimeout,
defaultItemOperationTimeout: defaultItemOperationTimeout,
defaultSnapshotLocations: defaultSnapshotLocations,
metrics: metrics,
backupStoreGetter: backupStoreGetter,
formatFlag: formatFlag,
credentialFileStore: credentialStore,
maxConcurrentK8SConnections: maxConcurrentK8SConnections,
defaultSnapshotMoveData: defaultSnapshotMoveData,
itemBlockWorkerCount: itemBlockWorkerCount,
concurrentBackups: max(concurrentBackups, 1),
globalCRClient: globalCRClient,
globalVolumePoliciesConfigMap: globalVolumePoliciesConfigMap,
}
b.updateTotalBackupMetric()
return b
@@ -587,9 +590,13 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel
request.Status.ValidationErrors = append(request.Status.ValidationErrors, "encountered labelSelector as well as orLabelSelectors in backup spec, only one can be specified")
}
resourcePolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(*request.Backup, b.kbClient, logger)
resourcePolicies, err := resourcepolicies.GetResourcePoliciesFromBackupWithGlobal(
*request.Backup, b.kbClient, b.globalVolumePoliciesConfigMap, request.Namespace, logger)
if err != nil {
request.Status.ValidationErrors = append(request.Status.ValidationErrors, err.Error())
} else if b.globalVolumePoliciesConfigMap != "" {
// Record the contributing global volume policies ConfigMap so `velero backup describe` can surface it.
request.Annotations[velerov1api.GlobalBackupVolumePolicyConfigMapAnnotation] = b.globalVolumePoliciesConfigMap
}
if resourcePolicies != nil && resourcePolicies.GetIncludeExcludePolicy() != nil && collections.UseOldResourceFilters(request.Spec) {
request.Status.ValidationErrors = append(request.Status.ValidationErrors, "include-resources, exclude-resources and include-cluster-resources are old filter parameters.\n"+
+95
View File
@@ -46,6 +46,7 @@ import (
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
fakeClient "sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
pkgbackup "github.com/vmware-tanzu/velero/pkg/backup"
"github.com/vmware-tanzu/velero/pkg/builder"
@@ -2076,6 +2077,100 @@ namespacedFilterPolicies:
assert.True(t, hasTargetError, "expected validation error about namespacedFilterPolicies incompatibility with old-style filters, got: %v", res.Status.ValidationErrors)
}
// TestPrepareBackupRequest_GlobalVolumePolicies verifies that the cluster-wide global backup
// volume policies are merged into the request and that the contributing ConfigMap is recorded
// on the backup so `velero backup describe` can surface it.
func TestPrepareBackupRequest_GlobalVolumePolicies(t *testing.T) {
formatFlag := logging.FormatText
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
globalCM := &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "global-volume-policy", Namespace: velerov1api.DefaultNamespace},
Data: map[string]string{"policies.yaml": `version: v1
volumePolicies:
- conditions:
storageClass:
- gp2
action:
type: skip
`},
}
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, globalCM,
builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "loc-1").Result())
apiServer := velerotest.NewAPIServer(t)
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
require.NoError(t, err)
c := &backupReconciler{
logger: logger,
discoveryHelper: discoveryHelper,
kbClient: fakeClient,
clock: &clock.RealClock{},
formatFlag: formatFlag,
defaultBackupLocation: "loc-1",
globalVolumePoliciesConfigMap: "global-volume-policy",
}
backup := defaultBackup().StorageLocation("loc-1").Result()
res := c.prepareBackupRequest(ctx, backup, logger)
defer res.WorkerPool.Stop()
// The global volume policies must load cleanly (no policy-related validation error).
for _, e := range res.Status.ValidationErrors {
assert.NotContains(t, e, "global backup volume policies")
}
require.NotNil(t, res.ResPolicies)
assert.Equal(t, "global-volume-policy", res.Annotations[velerov1api.GlobalBackupVolumePolicyConfigMapAnnotation])
action, err := res.ResPolicies.GetMatchAction(resourcepolicies.VolumeFilterData{
PersistentVolume: &corev1api.PersistentVolume{Spec: corev1api.PersistentVolumeSpec{StorageClassName: "gp2"}},
})
require.NoError(t, err)
require.NotNil(t, action)
assert.Equal(t, resourcepolicies.Skip, action.Type)
}
// TestPrepareBackupRequest_GlobalVolumePolicies_LoadError verifies that when the configured
// global backup volume policies ConfigMap cannot be loaded, a validation error is recorded and
// the contributing-ConfigMap annotation is not set on the backup.
func TestPrepareBackupRequest_GlobalVolumePolicies_LoadError(t *testing.T) {
formatFlag := logging.FormatText
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
// No ConfigMap with this name exists, so loading the global policies fails.
fakeClient := velerotest.NewFakeControllerRuntimeClient(t,
builder.ForBackupStorageLocation(velerov1api.DefaultNamespace, "loc-1").Result())
apiServer := velerotest.NewAPIServer(t)
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
require.NoError(t, err)
c := &backupReconciler{
logger: logger,
discoveryHelper: discoveryHelper,
kbClient: fakeClient,
clock: &clock.RealClock{},
formatFlag: formatFlag,
defaultBackupLocation: "loc-1",
globalVolumePoliciesConfigMap: "missing-global-volume-policy",
}
backup := defaultBackup().StorageLocation("loc-1").Result()
res := c.prepareBackupRequest(ctx, backup, logger)
defer res.WorkerPool.Stop()
// The failure to load the global policies must surface as a validation error.
var hasGlobalPolicyError bool
for _, e := range res.Status.ValidationErrors {
if strings.Contains(e, "global backup volume policies") {
hasGlobalPolicyError = true
}
}
assert.True(t, hasGlobalPolicyError, "expected a validation error about global backup volume policies, got: %v", res.Status.ValidationErrors)
// The annotation is only set when the policies load successfully.
assert.Empty(t, res.Annotations[velerov1api.GlobalBackupVolumePolicyConfigMapAnnotation])
}
// TestPrepareBackupRequest_ClusterScopedFilterPolicyIncompatibleWithOldFilters verifies
// that a backup referencing a ResourcePolicy ConfigMap with clusterScopedFilterPolicy
// produces a validation error when old-style resource filters are also set on the spec.