mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-07-29 19:42:45 +00:00
Merge branch 'main' into block-uploader-thread-module-refactor
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Add restore.velero.io/must-include-additional-items so RestoreItemActions can opt in to bypassing global restore filters for AdditionalItems (mirrors the backup-side must-include annotation; no default behavior change for existing restores/plugins)
|
||||
@@ -102,10 +102,5 @@ RUN ARCH=$(go env GOARCH) && \
|
||||
# release API/CDN, which has been returning intermittent/persistent HTTP 504s.
|
||||
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
|
||||
|
||||
# install kubectl
|
||||
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/$(go env GOARCH)/kubectl
|
||||
RUN chmod +x ./kubectl
|
||||
RUN mv ./kubectl /usr/local/bin
|
||||
|
||||
# Fix the "dubious ownership" issue from git when running goreleaser.sh
|
||||
RUN echo "[safe] \n\t directory = *" > /.gitconfig
|
||||
|
||||
@@ -166,6 +166,14 @@ const (
|
||||
|
||||
// Velero checks this annotation to determine whether to skip resource excluding check.
|
||||
MustIncludeAdditionalItemAnnotation = "backup.velero.io/must-include-additional-items"
|
||||
// MustIncludeAdditionalItemRestoreAnnotation is set by RestoreItemActions on the UpdatedItem
|
||||
// to tell Velero to bypass global resource/namespace exclusion checks (and IncludeClusterResources=false)
|
||||
// for that action's AdditionalItems. Value must be "true" to enable the bypass. The annotation is
|
||||
// always stripped before the item is applied to the cluster when present, including non-"true" values.
|
||||
//
|
||||
// Notice: SkipRestore on the Execute output takes precedence. If SkipRestore is true, the
|
||||
// annotation is never inspected and AdditionalItems are not processed.
|
||||
MustIncludeAdditionalItemRestoreAnnotation = "restore.velero.io/must-include-additional-items"
|
||||
// SkippedNoCSIPVAnnotation - Velero checks this annotation on processed PVC to
|
||||
// find out if the snapshot was skipped b/c the PV is not provisioned via CSI
|
||||
SkippedNoCSIPVAnnotation = "backup.velero.io/skipped-no-csi-pv"
|
||||
|
||||
+48
-23
@@ -1060,7 +1060,7 @@ func (ctx *restoreContext) processSelectedResource(
|
||||
continue
|
||||
}
|
||||
|
||||
w, e, _ := ctx.restoreItem(obj, groupResource, targetNS)
|
||||
w, e, _ := ctx.restoreItem(obj, groupResource, targetNS, false)
|
||||
warnings.Merge(&w)
|
||||
errs.Merge(&e)
|
||||
processedItems++
|
||||
@@ -1386,7 +1386,7 @@ func (ctx *restoreContext) getResource(groupResource schema.GroupResource, obj *
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupResource schema.GroupResource, namespace string) (results.Result, results.Result, bool) {
|
||||
func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupResource schema.GroupResource, namespace string, mustInclude bool) (results.Result, results.Result, bool) {
|
||||
warnings, errs := results.Result{}, results.Result{}
|
||||
// itemExists bool is used to determine whether to include this item in the "wait for additional items" list
|
||||
itemExists := false
|
||||
@@ -1403,27 +1403,41 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
// Check if group/resource should be restored. We need to do this here since
|
||||
// this method may be getting called for an additional item which is a group/resource
|
||||
// that's excluded.
|
||||
if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) {
|
||||
restoreLogger.Info("Not restoring item because resource is excluded")
|
||||
return warnings, errs, itemExists
|
||||
}
|
||||
|
||||
// Check if namespace/cluster-scoped resource should be restored. We need
|
||||
// to do this here since this method may be getting called for an additional
|
||||
// item which is in a namespace that's excluded, or which is cluster-scoped
|
||||
// and should be excluded. Note that we're checking the object's namespace (
|
||||
// via obj.GetNamespace()) instead of the namespace parameter, because we want
|
||||
// to check the *original* namespace, not the remapped one if it's been remapped.
|
||||
//
|
||||
// Note: Additional items intentionally bypass fine-grained resource filter policies
|
||||
// (like per-namespace label/name selectors) to avoid breaking semantic dependencies,
|
||||
// but they must still pass the global exclusions enforced below.
|
||||
if namespace != "" {
|
||||
if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) {
|
||||
restoreLogger.Info("Not restoring item because namespace is excluded")
|
||||
// but they must still pass the global exclusions enforced below unless mustInclude is set.
|
||||
if mustInclude {
|
||||
restoreLogger.Info("Skipping the resource/namespace exclusion checks because the item is marked as must-include")
|
||||
} else {
|
||||
if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) && !ctx.resourceMustHave.Has(groupResource.String()) {
|
||||
restoreLogger.Info("Not restoring item because resource is excluded")
|
||||
return warnings, errs, itemExists
|
||||
}
|
||||
|
||||
// Check if namespace/cluster-scoped resource should be restored. We need
|
||||
// to do this here since this method may be getting called for an additional
|
||||
// item which is in a namespace that's excluded, or which is cluster-scoped
|
||||
// and should be excluded. Note that we're checking the object's namespace (
|
||||
// via obj.GetNamespace()) instead of the namespace parameter, because we want
|
||||
// to check the *original* namespace, not the remapped one if it's been remapped.
|
||||
if namespace != "" {
|
||||
if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) {
|
||||
restoreLogger.Info("Not restoring item because namespace is excluded")
|
||||
return warnings, errs, itemExists
|
||||
}
|
||||
} else {
|
||||
if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) {
|
||||
restoreLogger.Info("Not restoring item because it's cluster-scoped")
|
||||
return warnings, errs, itemExists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace creation runs unconditionally when namespace != "", regardless of
|
||||
// mustInclude. This ensures target namespaces exist for additional items that
|
||||
// bypass the namespace-exclusion check above.
|
||||
if namespace != "" {
|
||||
// If the namespace scoped resource should be restored, ensure that the
|
||||
// namespace into which the resource is being restored into exists.
|
||||
// This is the *remapped* namespace that we are ensuring exists.
|
||||
@@ -1442,11 +1456,6 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
}
|
||||
ctx.restoredItems[itemKey] = restoredItemStatus{action: ItemRestoreResultCreated, itemExists: true, createdName: nsToEnsure.Name}
|
||||
}
|
||||
} else {
|
||||
if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) {
|
||||
restoreLogger.Info("Not restoring item because it's cluster-scoped")
|
||||
return warnings, errs, itemExists
|
||||
}
|
||||
}
|
||||
|
||||
// Make a copy of object retrieved from backup to make it available unchanged
|
||||
@@ -1668,6 +1677,21 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
|
||||
obj = unstructuredObj
|
||||
|
||||
mustIncludeAdditionalItems := false
|
||||
if annotations := obj.GetAnnotations(); annotations != nil {
|
||||
if _, present := annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation]; present {
|
||||
// Only the string value "true" enables the bypass.
|
||||
if annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] == "true" {
|
||||
mustIncludeAdditionalItems = true
|
||||
restoreLogger.Info("RestoreItemAction marked additional items as must-include; bypassing resource/namespace exclusion checks for them")
|
||||
}
|
||||
// Always strip the annotation so it never lands on the cluster,
|
||||
// regardless of whether the value enabled the bypass.
|
||||
delete(annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
|
||||
obj.SetAnnotations(annotations)
|
||||
}
|
||||
}
|
||||
|
||||
var filteredAdditionalItems []velero.ResourceIdentifier
|
||||
for _, additionalItem := range executeOutput.AdditionalItems {
|
||||
itemPath := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name)
|
||||
@@ -1687,6 +1711,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
additionalObj, err := archive.Unmarshal(ctx.fileSystem, itemPath)
|
||||
if err != nil {
|
||||
errs.Add(namespace, errors.Wrapf(err, "error restoring additional item %s", additionalResourceID))
|
||||
continue
|
||||
}
|
||||
|
||||
additionalItemNamespace := additionalItem.Namespace
|
||||
@@ -1696,7 +1721,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
|
||||
}
|
||||
}
|
||||
|
||||
w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace)
|
||||
w, e, additionalItemExists := ctx.restoreItem(additionalObj, additionalItem.GroupResource, additionalItemNamespace, mustIncludeAdditionalItems)
|
||||
if additionalItemExists {
|
||||
filteredAdditionalItems = append(filteredAdditionalItems, additionalItem)
|
||||
}
|
||||
|
||||
@@ -2150,6 +2150,102 @@ func TestRestoreActionAdditionalItems(t *testing.T) {
|
||||
test.PVs(): nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must-include annotation bypasses resource exclusion for additional items",
|
||||
restore: defaultRestore().IncludedResources("pods").Result(),
|
||||
backup: defaultBackup().Result(),
|
||||
tarball: test.NewTarWriter(t).
|
||||
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
|
||||
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
|
||||
Done(),
|
||||
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
|
||||
actions: []riav2.RestoreItemAction{
|
||||
&pluggableAction{
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
want: map[*test.APIResource][]string{
|
||||
test.Pods(): {"ns-1/pod-1"},
|
||||
test.PVs(): {"/pv-1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must-include annotation bypasses namespace exclusion for additional items",
|
||||
restore: defaultRestore().IncludedNamespaces("ns-1").Result(),
|
||||
backup: defaultBackup().Result(),
|
||||
tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).Done(),
|
||||
apiResources: []*test.APIResource{test.Pods()},
|
||||
actions: []riav2.RestoreItemAction{
|
||||
&pluggableAction{
|
||||
selector: velero.ResourceSelector{IncludedNamespaces: []string{"ns-1"}},
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.Pods, Namespace: "ns-2", Name: "pod-2"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
want: map[*test.APIResource][]string{
|
||||
test.Pods(): {"ns-1/pod-1", "ns-2/pod-2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must-include annotation bypasses IncludeClusterResources=false for additional items",
|
||||
restore: defaultRestore().IncludeClusterResources(false).Result(),
|
||||
backup: defaultBackup().Result(),
|
||||
tarball: test.NewTarWriter(t).
|
||||
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
|
||||
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
|
||||
Done(),
|
||||
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
|
||||
actions: []riav2.RestoreItemAction{
|
||||
&pluggableAction{
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
want: map[*test.APIResource][]string{
|
||||
test.Pods(): {"ns-1/pod-1"},
|
||||
test.PVs(): {"/pv-1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -2180,6 +2276,324 @@ func TestRestoreActionAdditionalItems(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRestoreMustIncludeAdditionalItems covers restore must-include edge cases beyond the
|
||||
// basic filter-bypass cases in TestRestoreActionAdditionalItems.
|
||||
func TestRestoreMustIncludeAdditionalItems(t *testing.T) {
|
||||
t.Run("must-include annotation is stripped from the restored item", func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.AddItems(t, test.Pods())
|
||||
|
||||
data := &Request{
|
||||
Log: h.log,
|
||||
Restore: defaultRestore().Result(),
|
||||
Backup: defaultBackup().Result(),
|
||||
BackupReader: test.NewTarWriter(t).
|
||||
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
|
||||
Done(),
|
||||
}
|
||||
warnings, errs := h.restorer.Restore(
|
||||
data,
|
||||
[]riav2.RestoreItemAction{
|
||||
&pluggableAction{
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
annotations["keep-me"] = "yes"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{UpdatedItem: item}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
assertEmptyResults(t, warnings, errs)
|
||||
|
||||
got, err := h.DynamicClient.Resource(test.Pods().GVR()).Namespace("ns-1").Get(t.Context(), "pod-1", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
annotations := got.GetAnnotations()
|
||||
assert.NotContains(t, annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
|
||||
assert.Equal(t, "yes", annotations["keep-me"])
|
||||
})
|
||||
|
||||
t.Run("non-true must-include annotation is stripped without bypassing filters", func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.AddItems(t, test.Pods())
|
||||
h.AddItems(t, test.PVs())
|
||||
|
||||
data := &Request{
|
||||
Log: h.log,
|
||||
Restore: defaultRestore().IncludedResources("pods").Result(),
|
||||
Backup: defaultBackup().Result(),
|
||||
BackupReader: test.NewTarWriter(t).
|
||||
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
|
||||
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
|
||||
Done(),
|
||||
}
|
||||
warnings, errs := h.restorer.Restore(
|
||||
data,
|
||||
[]riav2.RestoreItemAction{
|
||||
&pluggableAction{
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "True"
|
||||
annotations["keep-me"] = "yes"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
assertEmptyResults(t, warnings, errs)
|
||||
assertAPIContents(t, h, map[*test.APIResource][]string{
|
||||
test.Pods(): {"ns-1/pod-1"},
|
||||
test.PVs(): nil,
|
||||
})
|
||||
|
||||
got, err := h.DynamicClient.Resource(test.Pods().GVR()).Namespace("ns-1").Get(t.Context(), "pod-1", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
annotations := got.GetAnnotations()
|
||||
assert.NotContains(t, annotations, velerov1api.MustIncludeAdditionalItemRestoreAnnotation)
|
||||
assert.Equal(t, "yes", annotations["keep-me"])
|
||||
})
|
||||
|
||||
t.Run("SkipRestore supersedes must-include annotation and skips additional items", func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.AddItems(t, test.Pods())
|
||||
h.AddItems(t, test.PVs())
|
||||
|
||||
data := &Request{
|
||||
Log: h.log,
|
||||
Restore: defaultRestore().IncludedResources("pods").Result(),
|
||||
Backup: defaultBackup().Result(),
|
||||
BackupReader: test.NewTarWriter(t).
|
||||
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
|
||||
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
|
||||
Done(),
|
||||
}
|
||||
warnings, errs := h.restorer.Restore(
|
||||
data,
|
||||
[]riav2.RestoreItemAction{
|
||||
&pluggableAction{
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
SkipRestore: true,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
assertEmptyResults(t, warnings, errs)
|
||||
assertAPIContents(t, h, map[*test.APIResource][]string{
|
||||
test.Pods(): nil,
|
||||
test.PVs(): nil,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("must-include does not restore additional items missing from the backup tarball", func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.AddItems(t, test.Pods())
|
||||
h.AddItems(t, test.PVs())
|
||||
|
||||
data := &Request{
|
||||
Log: h.log,
|
||||
Restore: defaultRestore().IncludedResources("pods").Result(),
|
||||
Backup: defaultBackup().Result(),
|
||||
BackupReader: test.NewTarWriter(t).
|
||||
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
|
||||
Done(),
|
||||
}
|
||||
warnings, errs := h.restorer.Restore(
|
||||
data,
|
||||
[]riav2.RestoreItemAction{
|
||||
&pluggableAction{
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-missing"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
assertEmptyResults(t, errs)
|
||||
assertNonEmptyResults(t, "warning", warnings)
|
||||
assertAPIContents(t, h, map[*test.APIResource][]string{
|
||||
test.Pods(): {"ns-1/pod-1"},
|
||||
test.PVs(): nil,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("transitive must-include requires each RIA level to re-set the annotation", func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.AddItems(t, test.Pods())
|
||||
h.AddItems(t, test.PVs())
|
||||
h.AddItems(t, test.PVCs())
|
||||
|
||||
data := &Request{
|
||||
Log: h.log,
|
||||
Restore: defaultRestore().IncludedResources("pods").Result(),
|
||||
Backup: defaultBackup().Result(),
|
||||
BackupReader: test.NewTarWriter(t).
|
||||
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
|
||||
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
|
||||
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-1").Result()).
|
||||
Done(),
|
||||
}
|
||||
warnings, errs := h.restorer.Restore(
|
||||
data,
|
||||
[]riav2.RestoreItemAction{
|
||||
// Parent pod RIA force-includes the excluded PV.
|
||||
&pluggableAction{
|
||||
selector: velero.ResourceSelector{IncludedResources: []string{"pods"}},
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
// Child PV RIA also re-sets the annotation to force-include an excluded PVC.
|
||||
&pluggableAction{
|
||||
selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumes"}},
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns-2", Name: "pvc-1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
assertEmptyResults(t, warnings, errs)
|
||||
assertAPIContents(t, h, map[*test.APIResource][]string{
|
||||
test.Pods(): {"ns-1/pod-1"},
|
||||
test.PVs(): {"/pv-1"},
|
||||
test.PVCs(): {"ns-2/pvc-1"},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("without re-annotating, transitive additional items still respect filters", func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
h.AddItems(t, test.Pods())
|
||||
h.AddItems(t, test.PVs())
|
||||
h.AddItems(t, test.PVCs())
|
||||
|
||||
data := &Request{
|
||||
Log: h.log,
|
||||
Restore: defaultRestore().IncludedResources("pods").Result(),
|
||||
Backup: defaultBackup().Result(),
|
||||
BackupReader: test.NewTarWriter(t).
|
||||
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).
|
||||
AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()).
|
||||
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-1").Result()).
|
||||
Done(),
|
||||
}
|
||||
warnings, errs := h.restorer.Restore(
|
||||
data,
|
||||
[]riav2.RestoreItemAction{
|
||||
&pluggableAction{
|
||||
selector: velero.ResourceSelector{IncludedResources: []string{"pods"}},
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
item := input.Item.(*unstructured.Unstructured)
|
||||
annotations := item.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
annotations[velerov1api.MustIncludeAdditionalItemRestoreAnnotation] = "true"
|
||||
item.SetAnnotations(annotations)
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumes, Name: "pv-1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
// Child PV RIA returns an additional PVC but does NOT set must-include.
|
||||
&pluggableAction{
|
||||
selector: velero.ResourceSelector{IncludedResources: []string{"persistentvolumes"}},
|
||||
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: input.Item,
|
||||
AdditionalItems: []velero.ResourceIdentifier{
|
||||
{GroupResource: kuberesource.PersistentVolumeClaims, Namespace: "ns-2", Name: "pvc-1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
assertEmptyResults(t, warnings, errs)
|
||||
assertAPIContents(t, h, map[*test.APIResource][]string{
|
||||
test.Pods(): {"ns-1/pod-1"},
|
||||
test.PVs(): {"/pv-1"},
|
||||
test.PVCs(): nil,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestShouldRestore runs the ShouldRestore function for various permutations of
|
||||
// existing/nonexisting/being-deleted PVs, PVCs, and namespaces, and verifies the
|
||||
// result/error matches expectations.
|
||||
|
||||
@@ -65,6 +65,32 @@ order in which item action plugins are invoked. However, if a single binary impl
|
||||
they may be invoked in the order in which they are registered but it is best to not depend on this
|
||||
implementation. This is not guaranteed officially and the implementation can change at any time.
|
||||
|
||||
### Must-include additional items (Restore Item Actions)
|
||||
|
||||
Restore Item Actions may return `AdditionalItems` that Velero restores as dependencies of the current item.
|
||||
By default those additional items must still pass the restore's global resource and namespace include/exclude
|
||||
filters (and `IncludeClusterResources=false` for cluster-scoped resources).
|
||||
|
||||
To force-restore hard dependencies despite those filters, set the following annotation on the `UpdatedItem`
|
||||
returned from `Execute()`:
|
||||
|
||||
```
|
||||
restore.velero.io/must-include-additional-items: "true"
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- Only the string value `"true"` enables the bypass.
|
||||
- The annotation applies blanket to all `AdditionalItems` from that RIA invocation (not per-item).
|
||||
- Velero strips the annotation before applying the item to the cluster.
|
||||
- `SkipRestore: true` takes precedence: if set, the annotation is never inspected and `AdditionalItems` are not processed.
|
||||
- Must-include only bypasses filters; the additional item must still exist in the backup tarball.
|
||||
- When an additional item targets an excluded namespace, Velero may still create that target namespace so the item can be restored.
|
||||
- Cluster-scoped additional items are restored even when `IncludeClusterResources=false`.
|
||||
- Transitive force-include requires each RIA level to re-set the annotation on its own `UpdatedItem`.
|
||||
|
||||
This mirrors the backup-side annotation `backup.velero.io/must-include-additional-items` used by Backup Item Actions.
|
||||
Installing an RIA that sets this annotation is a trust decision: the plugin can restore resources outside the operator's restore filters.
|
||||
|
||||
## Plugin Logging
|
||||
|
||||
Velero provides a [logger][2] that can be used by plugins to log structured information to the main Velero server log or
|
||||
|
||||
Reference in New Issue
Block a user