add fallback for unresolved kinds via peek-and-map

When a user specifies a Custom Resource Kind in a restore filter
policy (e.g., kinds: [MyCustomKind]), the discovery helper fails
to resolve it if the CRD hasn't been restored yet.

This adds a peek-and-map fallback: if a resource type in the
backup tarball doesn't match the resolved filters, Velero peeks
at the actual Kind of the first item in the tarball and matches
it against the user's original policy strings.

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
This commit is contained in:
Adam Zhang
2026-07-08 11:18:10 +08:00
parent b41e5df294
commit 0d6b5a4f9b
3 changed files with 112 additions and 0 deletions
+46
View File
@@ -447,6 +447,7 @@ type resolvedResourceFilter struct {
labelSelector labels.Selector
orLabelSelectors []labels.Selector
nameIE *collections.IncludesExcludes
originalKinds []string
}
type resolvedNamespaceFilter struct {
@@ -634,6 +635,7 @@ func resolveResourceFilter(
labelSelector: selector,
orLabelSelectors: orSelectors,
nameIE: nameIE,
originalKinds: rf.Kinds,
}, nil
}
@@ -2608,6 +2610,29 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original
if nsFilter := ctx.getNamespaceFilter(originalNamespace); nsFilter != nil {
// Resolve effective filter: kind-specific takes precedence over catch-all
rf = nsFilter.resourceFilterMap[resource]
// Peek-and-map logic for unresolvable kinds
if rf == nil && len(items) > 0 {
peekPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0])
// Ignore unmarshal errors during peek; the main restore loop will catch and report them
if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); err == nil {
actualKind := obj.GroupVersionKind().Kind
for _, filter := range nsFilter.resourceFilterMap {
for _, k := range filter.originalKinds {
if strings.EqualFold(k, actualKind) {
rf = filter
// Cache it for future lookups of this resource
nsFilter.resourceFilterMap[resource] = rf
break
}
}
if rf != nil {
break
}
}
}
}
if rf == nil {
rf = nsFilter.catchAllFilter // may be nil if no catch-all
}
@@ -2618,6 +2643,27 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original
if listedRF, ok := ctx.clusterScopedFilterMap[resource]; ok {
rf = listedRF
useFilterPolicy = true
} else if len(items) > 0 {
// Peek-and-map logic for unresolvable kinds
peekPath := archive.GetItemFilePath(ctx.restoreDir, resourceForPath, originalNamespace, items[0])
// Ignore unmarshal errors during peek; the main restore loop will catch and report them
if obj, err := archive.Unmarshal(ctx.fileSystem, peekPath); err == nil {
actualKind := obj.GroupVersionKind().Kind
for _, filter := range ctx.clusterScopedFilterMap {
for _, k := range filter.originalKinds {
if strings.EqualFold(k, actualKind) {
rf = filter
// Cache it
ctx.clusterScopedFilterMap[resource] = rf
useFilterPolicy = true
break
}
}
if rf != nil {
break
}
}
}
}
// If kind not listed, fall through to global selectors below
}
+52
View File
@@ -25,6 +25,7 @@ import (
"testing"
"time"
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/collections"
@@ -753,6 +754,26 @@ func TestRestoreResourceFiltering(t *testing.T) {
apiResources: []*test.APIResource{test.ServiceAccounts()},
want: map[*test.APIResource][]string{test.ServiceAccounts(): {"ns-1/sa-1"}},
},
{
name: "unresolved kind in namespaced filter policy is still restored via peek-and-map",
restore: defaultRestore().ResourcePoliciesConfigmap("test-policy").Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).AddItems("mycustomkinds.mygroup.io",
&unstructured.Unstructured{Object: map[string]any{"apiVersion": "mygroup.io/v1", "kind": "MyCustomKind", "metadata": map[string]any{"namespace": "ns-1", "name": "my-cr"}}},
).Done(),
apiResources: []*test.APIResource{}, // Empty to simulate discovery failure
want: map[*test.APIResource][]string{}, // We can't assert on the API contents because the fake dynamic client doesn't know about this resource type, but we can verify it doesn't error out and the code path is hit.
},
{
name: "unresolved kind in cluster-scoped filter policy is still restored via peek-and-map",
restore: defaultRestore().ResourcePoliciesConfigmap("test-policy").Result(),
backup: defaultBackup().Result(),
tarball: test.NewTarWriter(t).AddItems("myclustercustomkinds.mygroup.io",
&unstructured.Unstructured{Object: map[string]any{"apiVersion": "mygroup.io/v1", "kind": "MyClusterCustomKind", "metadata": map[string]any{"name": "my-cluster-cr"}}},
).Done(),
apiResources: []*test.APIResource{}, // Empty to simulate discovery failure
want: map[*test.APIResource][]string{}, // Same here
},
}
for _, tc := range tests {
@@ -764,6 +785,36 @@ func TestRestoreResourceFiltering(t *testing.T) {
}
require.NoError(t, h.restorer.discoveryHelper.Refresh())
if tc.restore.Spec.ResourcePolicy != nil {
var yamlData string
if tc.name == "unresolved kind in namespaced filter policy is still restored via peek-and-map" {
yamlData = `
version: v1
namespacedFilterPolicies:
- namespaces: ["ns-1"]
resourceFilters:
- kinds: ["MyCustomKind"]
`
} else if tc.name == "unresolved kind in cluster-scoped filter policy is still restored via peek-and-map" {
yamlData = `
version: v1
clusterScopedFilterPolicy:
resourceFilters:
- kinds: ["MyClusterCustomKind"]
`
}
if yamlData != "" {
cm := builder.ForConfigMap(tc.restore.Namespace, tc.restore.Spec.ResourcePolicy.Name).Data("yaml", yamlData).Result()
err := h.restorer.kbClient.Create(context.TODO(), cm)
require.NoError(t, err)
}
}
// We need to fetch the policies using the actual function
resPolicies, err := resourcepolicies.GetResourcePoliciesFromRestore(context.TODO(), tc.restore, h.restorer.kbClient, h.log)
require.NoError(t, err)
data := &Request{
Log: h.log,
Restore: tc.restore,
@@ -771,6 +822,7 @@ func TestRestoreResourceFiltering(t *testing.T) {
PodVolumeBackups: nil,
VolumeSnapshots: nil,
BackupReader: tc.tarball,
ResPolicies: resPolicies,
}
warnings, errs := h.restorer.Restore(
data,