mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-07-29 19:42:45 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user