Merge branch 'main' into block-uploader-thread-module-refactor

This commit is contained in:
Lyndon-Li
2026-07-28 13:21:28 +08:00
6 changed files with 497 additions and 28 deletions
@@ -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)
-5
View File
@@ -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
+8
View File
@@ -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
View File
@@ -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)
}
+414
View File
@@ -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.
+26
View File
@@ -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