Merge pull request #10098 from shubham-pampattiwar/impl/default-resource-modifier
Run the E2E test on kind / setup-test-matrix (push) Successful in 6s
e2e-test-kind.yaml / extract (push) Failing after 12s
Run the E2E test on kind / get-go-version (push) Failing after 13s
Run the E2E test on kind / build (push) Skipped
Run the E2E test on kind / run-e2e-test (push) Skipped
push.yml / extract (push) Successful in 12s
Main CI / get-go-version (push) Successful in 13s
Main CI / Build (push) Failing after 23s

Implement server default restore resource modifier
This commit is contained in:
Shubham Pampattiwar
2026-08-03 18:17:14 -07:00
committed by GitHub
21 changed files with 618 additions and 219 deletions
@@ -0,0 +1 @@
Implement server default restore resource modifier
@@ -467,6 +467,14 @@ spec:
from. If specified, and BackupName is empty, Velero will restore
from the most recent successful backup created from this schedule.
type: string
skipDefaultResourceModifier:
description: |-
SkipDefaultResourceModifier controls whether the server-configured default
resource modifier is applied to this restore.
When true, the default modifier is skipped even if configured on the server.
Has no effect when a per-restore ResourceModifier is specified.
nullable: true
type: boolean
uploaderConfig:
description: UploaderConfig specifies the configuration for the restore.
nullable: true
File diff suppressed because one or more lines are too long
@@ -0,0 +1,18 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: default-restore-resource-modifiers
namespace: velero
data:
resource-modifiers.yaml: |
version: v1
resourceModifierRules:
- conditions:
groupResource: pods
mergePatches:
- patchData: |
metadata:
annotations:
k8s.ovn.org/pod-networks: null
k8s.v1.cni.cncf.io/network-status: null
k8s.v1.cni.cncf.io/networks-status: null
+8
View File
@@ -135,6 +135,14 @@ type RestoreSpec struct {
// +nullable
ResourcePolicy *corev1api.TypedLocalObjectReference `json:"resourcePolicy,omitempty"`
// SkipDefaultResourceModifier controls whether the server-configured default
// resource modifier is applied to this restore.
// When true, the default modifier is skipped even if configured on the server.
// Has no effect when a per-restore ResourceModifier is specified.
// +optional
// +nullable
SkipDefaultResourceModifier *bool `json:"skipDefaultResourceModifier,omitempty"`
// UploaderConfig specifies the configuration for the restore.
// +optional
// +nullable
@@ -1427,6 +1427,11 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
*out = new(corev1.TypedLocalObjectReference)
(*in).DeepCopyInto(*out)
}
if in.SkipDefaultResourceModifier != nil {
in, out := &in.SkipDefaultResourceModifier, &out.SkipDefaultResourceModifier
*out = new(bool)
**out = **in
}
if in.UploaderConfig != nil {
in, out := &in.UploaderConfig, &out.UploaderConfig
*out = new(UploaderConfigForRestore)
+6
View File
@@ -181,3 +181,9 @@ func (b *RestoreBuilder) ResourcePoliciesConfigmap(name string) *RestoreBuilder
}
return b
}
// SkipDefaultResourceModifier sets whether to skip the server default resource modifier.
func (b *RestoreBuilder) SkipDefaultResourceModifier(val bool) *RestoreBuilder {
b.object.Spec.SkipDefaultResourceModifier = &val
return b
}
+105 -97
View File
@@ -42,60 +42,61 @@ import (
// Options collects all the options for installing Velero into a Kubernetes cluster.
type Options struct {
Namespace string
Image string
BucketName string
Prefix string
ProviderName string
PodAnnotations flag.Map
PodLabels flag.Map
ServiceAccountAnnotations flag.Map
ServiceAccountName string
VeleroPodCPURequest string
VeleroPodMemRequest string
VeleroPodCPULimit string
VeleroPodMemLimit string
NodeAgentPodCPURequest string
NodeAgentPodMemRequest string
NodeAgentPodCPULimit string
NodeAgentPodMemLimit string
RestoreOnly bool
SecretFile string
NoSecret bool
DryRun bool
BackupStorageConfig flag.Map
VolumeSnapshotConfig flag.Map
UseNodeAgent bool
UseNodeAgentWindows bool
PrivilegedNodeAgent bool
Wait bool
UseVolumeSnapshots bool
DefaultRepoMaintenanceFrequency time.Duration
GarbageCollectionFrequency time.Duration
PodVolumeOperationTimeout time.Duration
Plugins flag.StringArray
NoDefaultBackupLocation bool
CRDsOnly bool
CACertFile string
Features string
DefaultVolumesToFsBackup bool
UploaderType string
DefaultSnapshotMoveData bool
CSISnapshotEarlyFrequentPolling bool
DisableInformerCache bool
ScheduleSkipImmediately bool
PodResources kubeutil.PodResources
KeepLatestMaintenanceJobs int
BackupRepoConfigMap string
RepoMaintenanceJobConfigMap string
NodeAgentConfigMap string
ItemBlockWorkerCount int
ConcurrentBackups int
NodeAgentDisableHostPath bool
kubeletRootDir string
Apply bool
ServerPriorityClassName string
NodeAgentPriorityClassName string
Namespace string
Image string
BucketName string
Prefix string
ProviderName string
PodAnnotations flag.Map
PodLabels flag.Map
ServiceAccountAnnotations flag.Map
ServiceAccountName string
VeleroPodCPURequest string
VeleroPodMemRequest string
VeleroPodCPULimit string
VeleroPodMemLimit string
NodeAgentPodCPURequest string
NodeAgentPodMemRequest string
NodeAgentPodCPULimit string
NodeAgentPodMemLimit string
RestoreOnly bool
SecretFile string
NoSecret bool
DryRun bool
BackupStorageConfig flag.Map
VolumeSnapshotConfig flag.Map
UseNodeAgent bool
UseNodeAgentWindows bool
PrivilegedNodeAgent bool
Wait bool
UseVolumeSnapshots bool
DefaultRepoMaintenanceFrequency time.Duration
GarbageCollectionFrequency time.Duration
PodVolumeOperationTimeout time.Duration
Plugins flag.StringArray
NoDefaultBackupLocation bool
CRDsOnly bool
CACertFile string
Features string
DefaultVolumesToFsBackup bool
UploaderType string
DefaultSnapshotMoveData bool
CSISnapshotEarlyFrequentPolling bool
DisableInformerCache bool
ScheduleSkipImmediately bool
PodResources kubeutil.PodResources
KeepLatestMaintenanceJobs int
BackupRepoConfigMap string
RepoMaintenanceJobConfigMap string
DefaultResourceModifierConfigMap string
NodeAgentConfigMap string
ItemBlockWorkerCount int
ConcurrentBackups int
NodeAgentDisableHostPath bool
kubeletRootDir string
Apply bool
ServerPriorityClassName string
NodeAgentPriorityClassName string
}
// BindFlags adds command line values to the options struct.
@@ -189,6 +190,12 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) {
o.RepoMaintenanceJobConfigMap,
"The name of ConfigMap containing repository maintenance Job configurations.",
)
flags.StringVar(
&o.DefaultResourceModifierConfigMap,
"default-resource-modifier-configmap",
o.DefaultResourceModifierConfigMap,
"The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores.",
)
flags.StringVar(
&o.NodeAgentConfigMap,
"node-agent-configmap",
@@ -298,49 +305,50 @@ func (o *Options) AsVeleroOptions() (*install.VeleroOptions, error) {
}
return &install.VeleroOptions{
Namespace: o.Namespace,
Image: o.Image,
ProviderName: o.ProviderName,
Bucket: o.BucketName,
Prefix: o.Prefix,
PodAnnotations: o.PodAnnotations.Data(),
PodLabels: o.PodLabels.Data(),
ServiceAccountAnnotations: o.ServiceAccountAnnotations.Data(),
ServiceAccountName: o.ServiceAccountName,
VeleroPodResources: veleroPodResources,
NodeAgentPodResources: nodeAgentPodResources,
SecretData: secretData,
RestoreOnly: o.RestoreOnly,
UseNodeAgent: o.UseNodeAgent,
UseNodeAgentWindows: o.UseNodeAgentWindows,
PrivilegedNodeAgent: o.PrivilegedNodeAgent,
UseVolumeSnapshots: o.UseVolumeSnapshots,
BSLConfig: o.BackupStorageConfig.Data(),
VSLConfig: o.VolumeSnapshotConfig.Data(),
DefaultRepoMaintenanceFrequency: o.DefaultRepoMaintenanceFrequency,
GarbageCollectionFrequency: o.GarbageCollectionFrequency,
PodVolumeOperationTimeout: o.PodVolumeOperationTimeout,
Plugins: o.Plugins,
NoDefaultBackupLocation: o.NoDefaultBackupLocation,
CACertData: caCertData,
Features: strings.Split(o.Features, ","),
DefaultVolumesToFsBackup: o.DefaultVolumesToFsBackup,
UploaderType: o.UploaderType,
DefaultSnapshotMoveData: o.DefaultSnapshotMoveData,
CSISnapshotEarlyFrequentPolling: o.CSISnapshotEarlyFrequentPolling,
DisableInformerCache: o.DisableInformerCache,
ScheduleSkipImmediately: o.ScheduleSkipImmediately,
PodResources: o.PodResources,
KeepLatestMaintenanceJobs: o.KeepLatestMaintenanceJobs,
BackupRepoConfigMap: o.BackupRepoConfigMap,
RepoMaintenanceJobConfigMap: o.RepoMaintenanceJobConfigMap,
NodeAgentConfigMap: o.NodeAgentConfigMap,
ItemBlockWorkerCount: o.ItemBlockWorkerCount,
ConcurrentBackups: o.ConcurrentBackups,
KubeletRootDir: o.kubeletRootDir,
NodeAgentDisableHostPath: o.NodeAgentDisableHostPath,
ServerPriorityClassName: o.ServerPriorityClassName,
NodeAgentPriorityClassName: o.NodeAgentPriorityClassName,
Namespace: o.Namespace,
Image: o.Image,
ProviderName: o.ProviderName,
Bucket: o.BucketName,
Prefix: o.Prefix,
PodAnnotations: o.PodAnnotations.Data(),
PodLabels: o.PodLabels.Data(),
ServiceAccountAnnotations: o.ServiceAccountAnnotations.Data(),
ServiceAccountName: o.ServiceAccountName,
VeleroPodResources: veleroPodResources,
NodeAgentPodResources: nodeAgentPodResources,
SecretData: secretData,
RestoreOnly: o.RestoreOnly,
UseNodeAgent: o.UseNodeAgent,
UseNodeAgentWindows: o.UseNodeAgentWindows,
PrivilegedNodeAgent: o.PrivilegedNodeAgent,
UseVolumeSnapshots: o.UseVolumeSnapshots,
BSLConfig: o.BackupStorageConfig.Data(),
VSLConfig: o.VolumeSnapshotConfig.Data(),
DefaultRepoMaintenanceFrequency: o.DefaultRepoMaintenanceFrequency,
GarbageCollectionFrequency: o.GarbageCollectionFrequency,
PodVolumeOperationTimeout: o.PodVolumeOperationTimeout,
Plugins: o.Plugins,
NoDefaultBackupLocation: o.NoDefaultBackupLocation,
CACertData: caCertData,
Features: strings.Split(o.Features, ","),
DefaultVolumesToFsBackup: o.DefaultVolumesToFsBackup,
UploaderType: o.UploaderType,
DefaultSnapshotMoveData: o.DefaultSnapshotMoveData,
CSISnapshotEarlyFrequentPolling: o.CSISnapshotEarlyFrequentPolling,
DisableInformerCache: o.DisableInformerCache,
ScheduleSkipImmediately: o.ScheduleSkipImmediately,
PodResources: o.PodResources,
KeepLatestMaintenanceJobs: o.KeepLatestMaintenanceJobs,
BackupRepoConfigMap: o.BackupRepoConfigMap,
RepoMaintenanceJobConfigMap: o.RepoMaintenanceJobConfigMap,
DefaultResourceModifierConfigMap: o.DefaultResourceModifierConfigMap,
NodeAgentConfigMap: o.NodeAgentConfigMap,
ItemBlockWorkerCount: o.ItemBlockWorkerCount,
ConcurrentBackups: o.ConcurrentBackups,
KubeletRootDir: o.kubeletRootDir,
NodeAgentDisableHostPath: o.NodeAgentDisableHostPath,
ServerPriorityClassName: o.ServerPriorityClassName,
NodeAgentPriorityClassName: o.NodeAgentPriorityClassName,
}, nil
}
+33 -26
View File
@@ -85,32 +85,33 @@ Notes:
}
type CreateOptions struct {
BackupName string
ScheduleName string
RestoreName string
RestoreVolumes flag.OptionalBool
PreserveNodePorts flag.OptionalBool
Labels flag.Map
Annotations flag.Map
IncludeNamespaces flag.StringArray
ExcludeNamespaces flag.StringArray
ExistingResourcePolicy string
IncludeResources flag.StringArray
ExcludeResources flag.StringArray
StatusIncludeResources flag.StringArray
StatusExcludeResources flag.StringArray
NamespaceMappings flag.Map
Selector flag.LabelSelector
OrSelector flag.OrLabelSelector
IncludeClusterResources flag.OptionalBool
Wait bool
AllowPartiallyFailed flag.OptionalBool
ItemOperationTimeout time.Duration
ResourceModifierConfigMap string
ResourcePoliciesConfigMap string
WriteSparseFiles flag.OptionalBool
ParallelFilesDownload int
client kbclient.WithWatch
BackupName string
ScheduleName string
RestoreName string
RestoreVolumes flag.OptionalBool
PreserveNodePorts flag.OptionalBool
Labels flag.Map
Annotations flag.Map
IncludeNamespaces flag.StringArray
ExcludeNamespaces flag.StringArray
ExistingResourcePolicy string
IncludeResources flag.StringArray
ExcludeResources flag.StringArray
StatusIncludeResources flag.StringArray
StatusExcludeResources flag.StringArray
NamespaceMappings flag.Map
Selector flag.LabelSelector
OrSelector flag.OrLabelSelector
IncludeClusterResources flag.OptionalBool
Wait bool
AllowPartiallyFailed flag.OptionalBool
ItemOperationTimeout time.Duration
ResourceModifierConfigMap string
ResourcePoliciesConfigMap string
SkipDefaultResourceModifier bool
WriteSparseFiles flag.OptionalBool
ParallelFilesDownload int
client kbclient.WithWatch
}
func NewCreateOptions() *CreateOptions {
@@ -164,6 +165,8 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.ResourcePoliciesConfigMap, "resource-policies-configmap", "", "Reference to the ConfigMap containing restore resource filter policies")
flags.BoolVar(&o.SkipDefaultResourceModifier, "skip-default-resource-modifier", false, "Skip applying the server-configured default resource modifier for this restore")
f = flags.VarPF(&o.WriteSparseFiles, "write-sparse-files", "", "Whether to write sparse files during restoring volumes")
f.NoOptDefVal = cmd.TRUE
@@ -362,6 +365,10 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
},
}
if o.SkipDefaultResourceModifier {
restore.Spec.SkipDefaultResourceModifier = boolptr.True()
}
if len([]string(o.StatusIncludeResources)) > 0 {
restore.Spec.RestoreStatus = &api.RestoreStatusSpec{
IncludedResources: o.StatusIncludeResources,
+2
View File
@@ -105,6 +105,7 @@ func TestCreateCommand(t *testing.T) {
flags.Parse([]string{"--item-operation-timeout", itemOperationTimeout})
flags.Parse([]string{"--resource-modifier-configmap", resourceModifierConfigMap})
flags.Parse([]string{"--resource-policies-configmap", ResourcePoliciesConfigMap})
flags.Parse([]string{"--skip-default-resource-modifier"})
flags.Parse([]string{"--write-sparse-files", writeSparseFiles})
flags.Parse([]string{"--parallel-files-download", "2"})
client := velerotest.NewFakeControllerRuntimeClient(t).(kbclient.WithWatch)
@@ -145,6 +146,7 @@ func TestCreateCommand(t *testing.T) {
require.Equal(t, itemOperationTimeout, o.ItemOperationTimeout.String())
require.Equal(t, resourceModifierConfigMap, o.ResourceModifierConfigMap)
require.Equal(t, ResourcePoliciesConfigMap, o.ResourcePoliciesConfigMap)
require.True(t, o.SkipDefaultResourceModifier)
require.Equal(t, writeSparseFiles, o.WriteSparseFiles.String())
require.Equal(t, parallel, o.ParallelFilesDownload)
})
+7
View File
@@ -182,6 +182,7 @@ type Config struct {
ItemBlockWorkerCount int
ConcurrentBackups int
GlobalBackupVolumePoliciesConfigMap string
DefaultResourceModifierConfigMap string
}
func GetDefaultConfig() *Config {
@@ -282,4 +283,10 @@ func (c *Config) BindFlags(flags *pflag.FlagSet) {
c.GlobalBackupVolumePoliciesConfigMap,
"The name of a ConfigMap in the Velero install namespace holding global backup volume policies that are merged into every backup. Optional.",
)
flags.StringVar(
&c.DefaultResourceModifierConfigMap,
"default-resource-modifier-configmap",
c.DefaultResourceModifierConfigMap,
"The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores. Ignored when a per-restore resource modifier is specified.",
)
}
+1
View File
@@ -881,6 +881,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
s.config.DisableInformerCache,
s.crClient,
s.config.ResourceTimeout,
s.config.DefaultResourceModifierConfigMap,
)
if err = r.SetupWithManager(s.mgr); err != nil {
+4
View File
@@ -219,6 +219,10 @@ func DescribeRestore(
DescribeResourceModifier(d, restore.Spec.ResourceModifier)
}
if boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) {
d.Printf("Skip Default Resource Modifier:\ttrue\n")
}
if restore.Spec.ResourcePolicy != nil {
d.Println()
DescribeResourcePolicies(d, restore.Spec.ResourcePolicy)
+69 -20
View File
@@ -55,6 +55,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
pkgrestore "github.com/vmware-tanzu/velero/pkg/restore"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/collections"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
"github.com/vmware-tanzu/velero/pkg/util/logging"
@@ -109,10 +110,11 @@ type restoreReconciler struct {
defaultItemOperationTimeout time.Duration
disableInformerCache bool
newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager
backupStoreGetter persistence.ObjectBackupStoreGetter
globalCrClient client.Client
resourceTimeout time.Duration
newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager
backupStoreGetter persistence.ObjectBackupStoreGetter
globalCrClient client.Client
resourceTimeout time.Duration
defaultResourceModifierConfigMap string
}
type backupInfo struct {
@@ -135,6 +137,7 @@ func NewRestoreReconciler(
disableInformerCache bool,
globalCrClient client.Client,
resourceTimeout time.Duration,
defaultResourceModifierConfigMap string,
) *restoreReconciler {
r := &restoreReconciler{
ctx: ctx,
@@ -154,8 +157,9 @@ func NewRestoreReconciler(
newPluginManager: newPluginManager,
backupStoreGetter: backupStoreGetter,
globalCrClient: globalCrClient,
resourceTimeout: resourceTimeout,
globalCrClient: globalCrClient,
resourceTimeout: resourceTimeout,
defaultResourceModifierConfigMap: defaultResourceModifierConfigMap,
}
// Move the periodical backup and restore metrics computing logic from controllers to here.
@@ -431,27 +435,72 @@ func (r *restoreReconciler) validateAndComplete(ctx context.Context, restore *ap
}
var resourceModifiers *resourcemodifiers.ResourceModifiers
if restore.Spec.ResourceModifier != nil && strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) {
ResourceModifierConfigMap := &corev1api.ConfigMap{}
err := r.kbClient.Get(ctx, client.ObjectKey{Namespace: restore.Namespace, Name: restore.Spec.ResourceModifier.Name}, ResourceModifierConfigMap)
if err != nil {
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name))
return backupInfo{}, nil, nil
if restore.Spec.ResourceModifier != nil {
if strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) {
resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, restore.Spec.ResourceModifier.Name, false)
if resourceModifiers == nil && len(restore.Status.ValidationErrors) > 0 {
return backupInfo{}, nil, nil
}
} else {
r.logger.Warnf("Unsupported resource modifier kind %q, only %q is supported", restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType)
}
resourceModifiers, err = resourcemodifiers.GetResourceModifiersFromConfig(ResourceModifierConfigMap)
if err != nil {
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name).Error())
return backupInfo{}, nil, nil
} else if err = resourceModifiers.Validate(); err != nil {
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors, errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name).Error())
return backupInfo{}, nil, nil
} else if r.defaultResourceModifierConfigMap != "" {
if boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) {
r.logger.Infof("Skipping default resource modifier configmap %s/%s as SkipDefaultResourceModifier is set", restore.Namespace, r.defaultResourceModifierConfigMap)
} else {
resourceModifiers = r.loadResourceModifierConfigMap(ctx, restore, r.defaultResourceModifierConfigMap, true)
}
r.logger.Infof("Retrieved Resource modifiers provided in configmap %s/%s", restore.Namespace, restore.Spec.ResourceModifier.Name)
}
return info, resourceModifiers, restoreResPolicies
}
// loadResourceModifierConfigMap loads and validates a resource modifier ConfigMap.
// When isDefault is true, errors are non-fatal (logged as warnings, returns nil).
// When isDefault is false, errors are added to restore.Status.ValidationErrors.
func (r *restoreReconciler) loadResourceModifierConfigMap(
ctx context.Context, restore *api.Restore, cmName string, isDefault bool,
) *resourcemodifiers.ResourceModifiers {
cm := &corev1api.ConfigMap{}
if err := r.kbClient.Get(ctx, client.ObjectKey{Namespace: restore.Namespace, Name: cmName}, cm); err != nil {
if isDefault {
r.logger.WithError(err).Warnf("Failed to retrieve default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName)
return nil
}
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
fmt.Sprintf("failed to get resource modifiers configmap %s/%s: %v", restore.Namespace, cmName, err))
return nil
}
modifiers, err := resourcemodifiers.GetResourceModifiersFromConfig(cm)
if err != nil {
if isDefault {
r.logger.WithError(err).Warnf("Error parsing default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName)
return nil
}
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s", restore.Namespace, cmName).Error())
return nil
}
if err = modifiers.Validate(); err != nil {
if isDefault {
r.logger.WithError(err).Warnf("Validation error in default resource modifier configmap %s/%s, skipping", restore.Namespace, cmName)
return nil
}
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s", restore.Namespace, cmName).Error())
return nil
}
source := "per-restore"
if isDefault {
source = "default"
}
r.logger.Infof("Retrieved %s resource modifiers from configmap %s/%s", source, restore.Namespace, cmName)
return modifiers
}
// backupXorScheduleProvided returns true if exactly one of BackupName and
// ScheduleName are non-empty for the restore, or false otherwise.
func backupXorScheduleProvided(restore *api.Restore) bool {
+184
View File
@@ -116,6 +116,7 @@ func TestFetchBackupInfo(t *testing.T) {
false,
fakeGlobalClient,
10*time.Minute,
"",
)
if test.backupStoreError == nil {
@@ -197,6 +198,7 @@ func TestProcessQueueItemSkips(t *testing.T) {
false,
fakeGlobalClient,
10*time.Minute,
"",
)
_, err := r.Reconcile(t.Context(), ctrl.Request{NamespacedName: types.NamespacedName{
@@ -579,6 +581,7 @@ func TestRestoreReconcile(t *testing.T) {
false,
fakeGlobalClient,
10*time.Minute,
"",
)
r.clock = clocktesting.NewFakeClock(now)
@@ -767,6 +770,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) {
false,
fakeGlobalClient,
10*time.Minute,
"",
)
restore := &velerov1api.Restore{
@@ -863,6 +867,7 @@ func TestValidateAndCompleteWithResourcePolicySpecified(t *testing.T) {
false,
fakeGlobalClient,
10*time.Minute,
"",
)
restore := &velerov1api.Restore{
@@ -992,6 +997,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
false,
fakeGlobalClient,
10*time.Minute,
"",
)
restore := &velerov1api.Restore{
@@ -1110,6 +1116,184 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) {
assert.Contains(t, restore3.Status.ValidationErrors[0], "Validation error in resource modifiers provided in configmap")
}
func TestValidateAndCompleteWithDefaultResourceModifier(t *testing.T) {
formatFlag := logging.FormatText
validCMData := map[string]string{
"modifiers.yaml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n mergePatches:\n - patchData: |\n metadata:\n annotations:\n k8s.ovn.org/pod-networks: null\n",
}
setupReconciler := func(t *testing.T, defaultCM string) *restoreReconciler {
t.Helper()
fakeClient := velerotest.NewFakeControllerRuntimeClient(t)
fakeGlobalClient := velerotest.NewFakeControllerRuntimeClient(t)
pluginManager := &pluginmocks.Manager{}
backupStore := &persistencemocks.BackupStore{}
r := NewRestoreReconciler(
t.Context(),
velerov1api.DefaultNamespace,
nil,
fakeClient,
velerotest.NewLogger(),
logrus.DebugLevel,
func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager },
NewFakeSingleObjectBackupStoreGetter(backupStore),
metrics.NewServerMetrics(),
formatFlag,
60*time.Minute,
false,
fakeGlobalClient,
10*time.Minute,
defaultCM,
)
location := builder.ForBackupStorageLocation("velero", "default").Provider("myCloud").Bucket("bucket").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
require.NoError(t, r.kbClient.Create(t.Context(), location))
require.NoError(t, r.kbClient.Create(t.Context(),
defaultBackup().ObjectMeta(builder.WithName("backup-1")).StorageLocation("default").Phase(velerov1api.BackupPhaseCompleted).Result(),
))
return r
}
newRestore := func(perRestoreCM string, skip *bool) *velerov1api.Restore {
restore := &velerov1api.Restore{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "restore-1",
},
Spec: velerov1api.RestoreSpec{
BackupName: "backup-1",
SkipDefaultResourceModifier: skip,
},
}
if perRestoreCM != "" {
restore.Spec.ResourceModifier = &corev1api.TypedLocalObjectReference{
Kind: resourcemodifiers.ConfigmapRefType,
Name: perRestoreCM,
}
}
return restore
}
t.Run("default modifier applied when no per-restore modifier", func(t *testing.T) {
r := setupReconciler(t, "default-rm")
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace},
Data: validCMData,
}))
restore := newRestore("", nil)
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.NotNil(t, rm)
assert.Empty(t, restore.Status.ValidationErrors)
})
t.Run("per-restore modifier takes exclusive precedence over default", func(t *testing.T) {
// Default ConfigMap does NOT exist, but per-restore does.
// If default were applied, it would fail. Per-restore should succeed.
r := setupReconciler(t, "nonexistent-default")
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "per-restore-rm", Namespace: velerov1api.DefaultNamespace},
Data: validCMData,
}))
restore := newRestore("per-restore-rm", nil)
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.NotNil(t, rm)
assert.Empty(t, restore.Status.ValidationErrors)
})
t.Run("skip default modifier when SkipDefaultResourceModifier is true", func(t *testing.T) {
r := setupReconciler(t, "default-rm")
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace},
Data: validCMData,
}))
skipTrue := true
restore := newRestore("", &skipTrue)
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.Nil(t, rm)
assert.Empty(t, restore.Status.ValidationErrors)
})
t.Run("default modifier with invalid data is non-fatal", func(t *testing.T) {
r := setupReconciler(t, "invalid-default")
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "invalid-default", Namespace: velerov1api.DefaultNamespace},
Data: map[string]string{
"modifiers.yaml": "not-valid-yaml: [",
},
}))
restore := newRestore("", nil)
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.Nil(t, rm)
assert.Empty(t, restore.Status.ValidationErrors)
})
t.Run("default modifier missing is non-fatal", func(t *testing.T) {
r := setupReconciler(t, "nonexistent-cm")
restore := newRestore("", nil)
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.Nil(t, rm)
assert.Empty(t, restore.Status.ValidationErrors)
})
t.Run("per-restore modifier missing is fatal", func(t *testing.T) {
r := setupReconciler(t, "")
restore := newRestore("nonexistent-cm", nil)
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.Nil(t, rm)
assert.NotEmpty(t, restore.Status.ValidationErrors)
assert.Contains(t, restore.Status.ValidationErrors[0], "failed to get resource modifiers configmap")
})
t.Run("no default configured and no per-restore modifier", func(t *testing.T) {
r := setupReconciler(t, "")
restore := newRestore("", nil)
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.Nil(t, rm)
assert.Empty(t, restore.Status.ValidationErrors)
})
t.Run("unsupported resource modifier kind does not apply default", func(t *testing.T) {
r := setupReconciler(t, "default-rm")
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "default-rm", Namespace: velerov1api.DefaultNamespace},
Data: validCMData,
}))
restore := newRestore("", nil)
restore.Spec.ResourceModifier = &corev1api.TypedLocalObjectReference{
Kind: "Secret",
Name: "some-secret",
}
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.Nil(t, rm)
assert.Empty(t, restore.Status.ValidationErrors)
})
t.Run("default modifier validation failure is non-fatal", func(t *testing.T) {
r := setupReconciler(t, "invalid-validation")
require.NoError(t, r.kbClient.Create(t.Context(), &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "invalid-validation", Namespace: velerov1api.DefaultNamespace},
Data: map[string]string{
"modifiers.yaml": "version: v1\nresourceModifierRules:\n- conditions:\n groupResource: pods\n patches:\n - operation: invalid\n path: \"/spec\"\n value: \"test\"\n",
},
}))
restore := newRestore("", nil)
_, rm, _ := r.validateAndComplete(t.Context(), restore)
assert.Nil(t, rm)
assert.Empty(t, restore.Status.ValidationErrors)
})
}
func TestBackupXorScheduleProvided(t *testing.T) {
r := &velerov1api.Restore{}
assert.False(t, backupXorScheduleProvided(r))
+42 -31
View File
@@ -34,37 +34,38 @@ import (
type podTemplateOption func(*podTemplateConfig)
type podTemplateConfig struct {
image string
envVars []corev1api.EnvVar
restoreOnly bool
annotations map[string]string
labels map[string]string
resources corev1api.ResourceRequirements
withSecret bool
defaultRepoMaintenanceFrequency time.Duration
garbageCollectionFrequency time.Duration
podVolumeOperationTimeout time.Duration
plugins []string
features []string
defaultVolumesToFsBackup bool
serviceAccountName string
uploaderType string
defaultSnapshotMoveData bool
csiSnapshotEarlyFrequentPolling bool
privilegedNodeAgent bool
disableInformerCache bool
scheduleSkipImmediately bool
podResources kube.PodResources
keepLatestMaintenanceJobs int
backupRepoConfigMap string
repoMaintenanceJobConfigMap string
nodeAgentConfigMap string
itemBlockWorkerCount int
concurrentBackups int
forWindows bool
kubeletRootDir string
nodeAgentDisableHostPath bool
priorityClassName string
image string
envVars []corev1api.EnvVar
restoreOnly bool
annotations map[string]string
labels map[string]string
resources corev1api.ResourceRequirements
withSecret bool
defaultRepoMaintenanceFrequency time.Duration
garbageCollectionFrequency time.Duration
podVolumeOperationTimeout time.Duration
plugins []string
features []string
defaultVolumesToFsBackup bool
serviceAccountName string
uploaderType string
defaultSnapshotMoveData bool
csiSnapshotEarlyFrequentPolling bool
privilegedNodeAgent bool
disableInformerCache bool
scheduleSkipImmediately bool
podResources kube.PodResources
keepLatestMaintenanceJobs int
backupRepoConfigMap string
repoMaintenanceJobConfigMap string
defaultResourceModifierConfigMap string
nodeAgentConfigMap string
itemBlockWorkerCount int
concurrentBackups int
forWindows bool
kubeletRootDir string
nodeAgentDisableHostPath bool
priorityClassName string
}
func WithImage(image string) podTemplateOption {
@@ -229,6 +230,12 @@ func WithRepoMaintenanceJobConfigMap(repoMaintenanceJobConfigMap string) podTemp
}
}
func WithDefaultResourceModifierConfigMap(name string) podTemplateOption {
return func(c *podTemplateConfig) {
c.defaultResourceModifierConfigMap = name
}
}
func WithItemBlockWorkerCount(itemBlockWorkerCount int) podTemplateOption {
return func(c *podTemplateConfig) {
c.itemBlockWorkerCount = itemBlockWorkerCount
@@ -350,6 +357,10 @@ func Deployment(namespace string, opts ...podTemplateOption) *appsv1api.Deployme
args = append(args, fmt.Sprintf("--repo-maintenance-job-configmap=%s", c.repoMaintenanceJobConfigMap))
}
if len(c.defaultResourceModifierConfigMap) > 0 {
args = append(args, fmt.Sprintf("--default-resource-modifier-configmap=%s", c.defaultResourceModifierConfigMap))
}
if c.itemBlockWorkerCount > 0 {
args = append(args, fmt.Sprintf("--item-block-worker-count=%d", c.itemBlockWorkerCount))
}
+4
View File
@@ -109,6 +109,10 @@ func TestDeployment(t *testing.T) {
assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2)
assert.Equal(t, "--repo-maintenance-job-configmap=test-repo-maintenance-config", deploy.Spec.Template.Spec.Containers[0].Args[1])
deploy = Deployment("velero", WithDefaultResourceModifierConfigMap("default-restore-modifiers"))
assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2)
assert.Equal(t, "--default-resource-modifier-configmap=default-restore-modifiers", deploy.Spec.Template.Spec.Containers[0].Args[1])
assert.Equal(t, &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
+48 -43
View File
@@ -234,49 +234,50 @@ func appendUnstructured(list *unstructured.UnstructuredList, obj runtime.Object)
}
type VeleroOptions struct {
Namespace string
Image string
ProviderName string
Bucket string
Prefix string
PodAnnotations map[string]string
PodLabels map[string]string
ServiceAccountAnnotations map[string]string
ServiceAccountName string
VeleroPodResources corev1api.ResourceRequirements
NodeAgentPodResources corev1api.ResourceRequirements
SecretData []byte
RestoreOnly bool
UseNodeAgent bool
UseNodeAgentWindows bool
PrivilegedNodeAgent bool
UseVolumeSnapshots bool
BSLConfig map[string]string
VSLConfig map[string]string
DefaultRepoMaintenanceFrequency time.Duration
GarbageCollectionFrequency time.Duration
PodVolumeOperationTimeout time.Duration
Plugins []string
NoDefaultBackupLocation bool
CACertData []byte
Features []string
DefaultVolumesToFsBackup bool
UploaderType string
DefaultSnapshotMoveData bool
CSISnapshotEarlyFrequentPolling bool
DisableInformerCache bool
ScheduleSkipImmediately bool
PodResources kube.PodResources
KeepLatestMaintenanceJobs int
BackupRepoConfigMap string
RepoMaintenanceJobConfigMap string
NodeAgentConfigMap string
ItemBlockWorkerCount int
ConcurrentBackups int
KubeletRootDir string
NodeAgentDisableHostPath bool
ServerPriorityClassName string
NodeAgentPriorityClassName string
Namespace string
Image string
ProviderName string
Bucket string
Prefix string
PodAnnotations map[string]string
PodLabels map[string]string
ServiceAccountAnnotations map[string]string
ServiceAccountName string
VeleroPodResources corev1api.ResourceRequirements
NodeAgentPodResources corev1api.ResourceRequirements
SecretData []byte
RestoreOnly bool
UseNodeAgent bool
UseNodeAgentWindows bool
PrivilegedNodeAgent bool
UseVolumeSnapshots bool
BSLConfig map[string]string
VSLConfig map[string]string
DefaultRepoMaintenanceFrequency time.Duration
GarbageCollectionFrequency time.Duration
PodVolumeOperationTimeout time.Duration
Plugins []string
NoDefaultBackupLocation bool
CACertData []byte
Features []string
DefaultVolumesToFsBackup bool
UploaderType string
DefaultSnapshotMoveData bool
CSISnapshotEarlyFrequentPolling bool
DisableInformerCache bool
ScheduleSkipImmediately bool
PodResources kube.PodResources
KeepLatestMaintenanceJobs int
BackupRepoConfigMap string
RepoMaintenanceJobConfigMap string
DefaultResourceModifierConfigMap string
NodeAgentConfigMap string
ItemBlockWorkerCount int
ConcurrentBackups int
KubeletRootDir string
NodeAgentDisableHostPath bool
ServerPriorityClassName string
NodeAgentPriorityClassName string
}
func AllCRDs() *unstructured.UnstructuredList {
@@ -407,6 +408,10 @@ func AllResources(o *VeleroOptions) *unstructured.UnstructuredList {
deployOpts = append(deployOpts, WithRepoMaintenanceJobConfigMap(o.RepoMaintenanceJobConfigMap))
}
if len(o.DefaultResourceModifierConfigMap) > 0 {
deployOpts = append(deployOpts, WithDefaultResourceModifierConfigMap(o.DefaultResourceModifierConfigMap))
}
deploy := Deployment(o.Namespace, deployOpts...)
if err := appendUnstructured(resources, deploy); err != nil {
+26
View File
@@ -118,6 +118,32 @@ func TestAllResources(t *testing.T) {
assert.Len(t, ds, 2)
}
func TestAllResourcesWithDefaultResourceModifierConfigMap(t *testing.T) {
option := &VeleroOptions{
Namespace: "velero",
SecretData: []byte{'a'},
DefaultResourceModifierConfigMap: "default-rm",
}
list := AllResources(option)
for _, item := range list.Items {
if item.GetKind() == "Deployment" && item.GetName() == "velero" {
containers, _, _ := unstructured.NestedSlice(item.Object, "spec", "template", "spec", "containers")
args, _, _ := unstructured.NestedStringSlice(containers[0].(map[string]any), "args")
found := false
for _, arg := range args {
if arg == "--default-resource-modifier-configmap=default-rm" {
found = true
break
}
}
assert.True(t, found, "expected --default-resource-modifier-configmap=default-rm in deployment args")
return
}
}
t.Fatal("velero deployment not found in AllResources output")
}
func TestAllResourcesWithPriorityClassName(t *testing.T) {
testCases := []struct {
name string
@@ -501,6 +501,7 @@ By far, `velero install` supports the following parameters to specify the extern
* --backup-repository-configmap: [backup repository configuration document][15]
* --node-agent-configmap: [node-agent concurrency configuration document][16], and there are some other documents specify other parts of node-agent-config.
* --repo-maintenance-job-configmap: [repository maintenance configuration document][17]
* --default-resource-modifier-configmap: [default restore resource modifier document][18]. When set, the referenced ConfigMap's resource modifier rules apply automatically to all restores that don't specify a per-restore modifier.
From v1.17, Velero adds verification for the ConfigMaps in CLI and server side, which means `velero install` CLI will fail and velero server and node-agent pod will exit if the specified ConfigMaps don't exist or are invalid.
@@ -539,3 +540,4 @@ The new workflow is:
[15]: backup-repository-configuration.md
[16]: node-agent-concurrency.md
[17]: repository-maintenance.md
[18]: restore-resource-modifiers.md#default-resource-modifiers
@@ -184,4 +184,47 @@ resourceModifierRules:
### Wildcard Support for GroupResource
The user can specify a wildcard for groupResource in the conditions' struct. This will allow the user to apply the patches for all the resources of a particular group or all resources in all groups. For example, `*.apps` will apply to all the resources in the `apps` group, `*` will apply to all the resources in core group, `*.*` will apply to all the resources in all groups.
- If both `*.groupName` and `namespaces` are specified, the patches will be applied to all the namespaced resources in this group in the specified namespaces and all the cluster resources in this group.
- If both `*.groupName` and `namespaces` are specified, the patches will be applied to all the namespaced resources in this group in the specified namespaces and all the cluster resources in this group.
## Default Resource Modifiers
Velero supports a server-level default resource modifier that applies automatically to all restores without requiring per-restore configuration.
This is useful for common transformations like stripping stale CNI annotations that can break workloads after restore.
### Configuration
1. Create a ConfigMap in the Velero namespace with your default resource modifier rules:
```bash
kubectl apply -f examples/default-resource-modifier-cni.yaml
```
2. Configure the Velero server to use it, either during install:
```bash
velero install --default-resource-modifier-configmap=default-restore-resource-modifiers ...
```
Or by editing an existing deployment:
```bash
kubectl -n velero edit deploy velero
# Add to the server args: --default-resource-modifier-configmap=default-restore-resource-modifiers
```
### Precedence
When a per-restore modifier is specified via `--resource-modifier-configmap`, it takes exclusive precedence and the default is not applied.
### Opt-out
To skip the default modifier for a specific restore without specifying a per-restore modifier:
```bash
velero restore create --from-backup my-backup --skip-default-resource-modifier
```
### Error Handling
If the default ConfigMap is missing or contains invalid data, Velero logs a warning and proceeds with the restore.
Per-restore modifier errors remain fatal and cause the restore to fail validation.