Files
velero/pkg/backup/item_collector_test.go
T
Adam ZhangandGitHub 85c660612b
Run the E2E test on kind / setup-test-matrix (push) Failing after 4s
e2e-test-kind.yaml / extract (push) Failing after 10s
Run the E2E test on kind / get-go-version (push) Failing after 11s
Run the E2E test on kind / build (push) Skipped
Run the E2E test on kind / run-e2e-test (push) Skipped
push.yml / extract (push) Failing after 6s
Main CI / get-go-version (push) Failing after 7s
Main CI / Build (push) Skipped
Enforce resource filters on cluster-wide items (#10455)
* Enforce resource filters on cluster-wide items

When backups query all namespaces (wildcard or omitted includes),
the item collector retrieved resources in bulk, bypassing
per-namespace resource filter policies in Stage 1 collection.
This caused resources not listed in the policy to be backed up.

To preserve cluster-wide query performance while enforcing policy
rules, evaluate namespace exclusions, resource kind allowlists,
and label selectors in memory for each collected item.

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>

* Optimize in-memory resource filter checks

Optimize per-item filter evaluation in the item collector:
- Precalculate GroupResource string once per resource type
- Skip filter policy evaluation when no namespaced policies exist
- Restrict in-memory filtering to cluster-wide queries
- Cache consecutive namespace lookups across collected items
- Lazily extract resource labels only when selectors are present

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>

---------

Signed-off-by: Adam Zhang <adam.zhang@broadcom.com>
2026-09-02 12:03:06 -04:00

656 lines
22 KiB
Go

/*
Copyright 2017, 2019, 2020 the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package backup
import (
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/vmware-tanzu/velero/pkg/test"
"github.com/vmware-tanzu/velero/pkg/util/collections"
)
func TestSortCoreGroup(t *testing.T) {
group := &metav1.APIResourceList{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{Name: "persistentvolumes"},
{Name: "configmaps"},
{Name: "antelopes"},
{Name: "persistentvolumeclaims"},
{Name: "pods"},
},
}
sortCoreGroup(group)
expected := []string{
"pods",
"persistentvolumeclaims",
"persistentvolumes",
"configmaps",
"antelopes",
}
for i, r := range group.APIResources {
assert.Equal(t, expected[i], r.Name)
}
}
func TestSortOrderedResource(t *testing.T) {
log := logrus.StandardLogger()
podResources := []*kubernetesResource{
{namespace: "ns1", name: "pod3"},
{namespace: "ns1", name: "pod1"},
{namespace: "ns1", name: "pod2"},
}
order := []string{"ns1/pod2", "ns1/pod1"}
expectedResources := []*kubernetesResource{
{namespace: "ns1", name: "pod2", orderedResource: true},
{namespace: "ns1", name: "pod1", orderedResource: true},
{namespace: "ns1", name: "pod3"},
}
sortedResources := sortResourcesByOrder(log, podResources, order)
assert.Equal(t, expectedResources, sortedResources)
// Test cluster resources
pvResources := []*kubernetesResource{
{name: "pv1"},
{name: "pv2"},
{name: "pv3"},
}
pvOrder := []string{"pv5", "pv2", "pv1"}
expectedPvResources := []*kubernetesResource{
{name: "pv2", orderedResource: true},
{name: "pv1", orderedResource: true},
{name: "pv3"},
}
sortedPvResources := sortResourcesByOrder(log, pvResources, pvOrder)
assert.Equal(t, expectedPvResources, sortedPvResources)
}
func TestFilterNamespaces(t *testing.T) {
tests := []struct {
name string
resources []*kubernetesResource
needToTrack string
expectedResources []*kubernetesResource
}{
{
name: "Namespace include by the filter but not in namespacesContainResource",
resources: []*kubernetesResource{
{
groupResource: kuberesource.Namespaces,
preferredGVR: kuberesource.Namespaces.WithVersion("v1"),
name: "ns1",
},
{
groupResource: kuberesource.Namespaces,
preferredGVR: kuberesource.Namespaces.WithVersion("v1"),
name: "ns2",
},
{
groupResource: kuberesource.Pods,
preferredGVR: kuberesource.Namespaces.WithVersion("v1"),
name: "pod1",
},
},
needToTrack: "ns1",
expectedResources: []*kubernetesResource{
{
groupResource: kuberesource.Namespaces,
preferredGVR: kuberesource.Namespaces.WithVersion("v1"),
name: "ns1",
},
{
groupResource: kuberesource.Pods,
preferredGVR: kuberesource.Namespaces.WithVersion("v1"),
name: "pod1",
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(*testing.T) {
r := itemCollector{
backupRequest: &Request{},
}
if tc.needToTrack != "" {
r.nsTracker.track(tc.needToTrack)
}
require.Equal(t, tc.expectedResources, r.nsTracker.filterNamespaces(tc.resources))
})
}
}
func TestItemCollectorBackupNamespaces(t *testing.T) {
tests := []struct {
name string
ie *collections.NamespaceIncludesExcludes
namespaces []*corev1api.Namespace
backup *velerov1api.Backup
expectedTrackedNS []string
converter runtime.UnstructuredConverter
}{
{
name: "ns filter by namespace IE filter",
backup: builder.ForBackup("velero", "backup").Result(),
ie: collections.NewNamespaceIncludesExcludes().Includes("ns1"),
namespaces: []*corev1api.Namespace{
builder.ForNamespace("ns1").Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
},
expectedTrackedNS: []string{"ns1"},
},
{
name: "ns filter by backup labelSelector",
backup: builder.ForBackup("velero", "backup").LabelSelector(&metav1.LabelSelector{
MatchLabels: map[string]string{"name": "ns1"},
}).Result(),
ie: collections.NewNamespaceIncludesExcludes().Includes("*"),
namespaces: []*corev1api.Namespace{
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
},
expectedTrackedNS: []string{"ns1"},
},
{
name: "ns filter by backup orLabelSelector",
backup: builder.ForBackup("velero", "backup").OrLabelSelector([]*metav1.LabelSelector{
{MatchLabels: map[string]string{"name": "ns1"}},
}).Result(),
ie: collections.NewNamespaceIncludesExcludes().Includes("*"),
namespaces: []*corev1api.Namespace{
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
},
expectedTrackedNS: []string{"ns1"},
},
{
name: "ns not included by IE filter, but included by labelSelector",
backup: builder.ForBackup("velero", "backup").LabelSelector(&metav1.LabelSelector{
MatchLabels: map[string]string{"name": "ns1"},
}).Result(),
ie: collections.NewNamespaceIncludesExcludes().Excludes("ns1"),
namespaces: []*corev1api.Namespace{
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
},
expectedTrackedNS: []string{"ns1"},
},
{
name: "ns not included by IE filter, but included by orLabelSelector",
backup: builder.ForBackup("velero", "backup").OrLabelSelector([]*metav1.LabelSelector{
{MatchLabels: map[string]string{"name": "ns1"}},
}).Result(),
ie: collections.NewNamespaceIncludesExcludes().Excludes("ns1", "ns2"),
namespaces: []*corev1api.Namespace{
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns3").Phase(corev1api.NamespaceActive).Result(),
},
expectedTrackedNS: []string{"ns1", "ns3"},
},
{
name: "No ns filters",
backup: builder.ForBackup("velero", "backup").Result(),
ie: collections.NewNamespaceIncludesExcludes().Includes("*"),
namespaces: []*corev1api.Namespace{
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
},
expectedTrackedNS: []string{"ns1", "ns2"},
},
{
name: "ns specified by the IncludeNamespaces cannot be found",
backup: builder.ForBackup("velero", "backup").IncludedNamespaces("ns1", "invalid", "*").Result(),
ie: collections.NewNamespaceIncludesExcludes().Includes("ns1", "invalid", "*"),
namespaces: []*corev1api.Namespace{
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
builder.ForNamespace("ns3").Phase(corev1api.NamespaceActive).Result(),
},
expectedTrackedNS: []string{"ns1"},
},
{
name: "terminating ns should not tracked",
backup: builder.ForBackup("velero", "backup").Result(),
ie: collections.NewNamespaceIncludesExcludes().Includes("ns1", "ns2"),
namespaces: []*corev1api.Namespace{
builder.ForNamespace("ns1").Phase(corev1api.NamespaceTerminating).Result(),
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
},
expectedTrackedNS: []string{"ns2"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(*testing.T) {
tempDir := t.TempDir()
var unstructuredNSList unstructured.UnstructuredList
for _, ns := range tc.namespaces {
unstructuredNS, err := runtime.DefaultUnstructuredConverter.ToUnstructured(ns)
require.NoError(t, err)
unstructuredNSList.Items = append(unstructuredNSList.Items,
unstructured.Unstructured{Object: unstructuredNS})
}
dc := &test.FakeDynamicClient{}
dc.On("List", mock.Anything).Return(&unstructuredNSList, nil)
factory := &test.FakeDynamicFactory{}
factory.On(
"ClientForGroupVersionResource",
mock.Anything,
mock.Anything,
mock.Anything,
).Return(dc, nil)
r := itemCollector{
backupRequest: &Request{
Backup: tc.backup,
NamespaceIncludesExcludes: tc.ie,
},
dynamicFactory: factory,
discoveryHelper: test.NewFakeDiscoveryHelper(true, nil),
dir: tempDir,
}
if tc.converter == nil {
tc.converter = runtime.DefaultUnstructuredConverter
}
r.collectNamespaces(
metav1.APIResource{
Name: "Namespace",
Kind: "Namespace",
Namespaced: false,
},
kuberesource.Namespaces.WithVersion("").GroupVersion(),
kuberesource.Namespaces,
kuberesource.Namespaces.WithVersion(""),
logrus.StandardLogger(),
)
for _, ns := range tc.expectedTrackedNS {
require.True(t, r.nsTracker.isTracked(ns))
}
})
}
}
// TestNamespacedFilterMap_GlobalExclusionPrecedence verifies the precedence rule:
// ResourceIncludesExcludes (set by includeExcludePolicy) is checked before the
// NamespacedFilterMap. This is enforced at both Stage 1 (item_collector.go line ~430)
// and Stage 2 (item_backupper.go itemInclusionChecks). The unit below confirms that
// GetNamespaceFilter still returns a filter for the namespace — it is the caller's
// responsibility to check ResourceIncludesExcludes first, which item_collector does.
//
// Full coverage of the Stage 2 enforcement is in item_backupper_test.go
// TestItemInclusionChecks_GlobalExclusion_OverridesNamespaceFilter.
func TestNamespacedFilterMap_GlobalExclusionPrecedence(t *testing.T) {
req := &Request{
Backup: builder.ForBackup("velero", "test-backup").Result(),
NamespaceIncludesExcludes: collections.NewNamespaceIncludesExcludes().Includes("ns-a"),
NamespacedFilterMap: map[string]*ResolvedNamespaceFilter{
"ns-a": {
ResourceFilterMap: map[string]*ResolvedResourceFilter{
"secrets.": {},
},
},
},
NamespacedFilterPatterns: []NamespacedFilterPattern{},
}
// GetNamespaceFilter returns the filter regardless of global exclusions.
// The caller (item_collector) is responsible for checking ResourceIncludesExcludes first.
nsFilter := req.GetNamespaceFilter("ns-a")
require.NotNil(t, nsFilter, "GetNamespaceFilter should return a filter for ns-a")
_, hasSecrets := nsFilter.ResourceFilterMap["secrets."]
assert.True(t, hasSecrets, "ns-a filter should list secrets GR")
// When a global excludeAllIE is set, item_collector would return nil before consulting the map.
// This is verified by the Stage 1 check: ShouldInclude("secrets.") == false → skip.
ie := &excludeAllIE{}
assert.False(t, ie.ShouldInclude("secrets."),
"global exclusion must reject secrets before the per-namespace filter is consulted")
}
// excludeAllIE is an IncludesExcludesInterface that excludes every resource kind.
type excludeAllIE struct{}
func (excludeAllIE) ShouldInclude(string) bool { return false }
func (excludeAllIE) ShouldExclude(string) bool { return true }
func TestGetResourceItems(t *testing.T) {
tests := []struct {
name string
namespaces []string
clusterScopedFilterMap map[string]*ResolvedResourceFilter
namespacedFilterMap map[string]*ResolvedNamespaceFilter
resource metav1.APIResource
gr schema.GroupResource
}{
{
name: "cluster scoped resource with filter",
namespaces: []string{""},
resource: metav1.APIResource{
Name: "persistentvolumes",
Namespaced: false,
},
gr: schema.GroupResource{Resource: "persistentvolumes"},
clusterScopedFilterMap: map[string]*ResolvedResourceFilter{
"persistentvolumes": {
LabelSelector: labels.Set{"app": "foo"}.AsSelector(),
},
},
},
{
name: "namespace scoped resource with filter",
namespaces: []string{"ns1"},
resource: metav1.APIResource{
Name: "pods",
Namespaced: true,
},
gr: schema.GroupResource{Resource: "pods"},
namespacedFilterMap: map[string]*ResolvedNamespaceFilter{
"ns1": {
ResourceFilterMap: map[string]*ResolvedResourceFilter{
"pods": {
LabelSelector: labels.Set{"app": "bar"}.AsSelector(),
},
},
},
},
},
{
name: "namespace scoped resource skipped due to no filter match",
namespaces: []string{"ns1"},
resource: metav1.APIResource{
Name: "secrets",
Namespaced: true,
},
gr: schema.GroupResource{Resource: "secrets"},
namespacedFilterMap: map[string]*ResolvedNamespaceFilter{
"ns1": {
ResourceFilterMap: map[string]*ResolvedResourceFilter{
"pods": {
LabelSelector: labels.Set{"app": "bar"}.AsSelector(),
},
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
dc := &test.FakeDynamicClient{}
dc.On("List", mock.Anything).Return(&unstructured.UnstructuredList{}, nil)
factory := &test.FakeDynamicFactory{}
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(dc, nil)
req := &Request{
Backup: builder.ForBackup("velero", "backup").Result(),
ClusterScopedFilterMap: tc.clusterScopedFilterMap,
NamespacedFilterMap: tc.namespacedFilterMap,
ResourceIncludesExcludes: includeAllIE{},
}
if len(tc.namespaces) > 0 && tc.namespaces[0] != "" {
req.NamespaceIncludesExcludes = collections.NewNamespaceIncludesExcludes().Includes(tc.namespaces...)
} else {
req.NamespaceIncludesExcludes = collections.NewNamespaceIncludesExcludes().Includes("*")
}
r := &itemCollector{
backupRequest: req,
dynamicFactory: factory,
discoveryHelper: test.NewFakeDiscoveryHelper(true, nil),
log: test.NewLogger(),
}
_, err := r.getResourceItems(test.NewLogger(), schema.GroupVersion{}, tc.resource, nil)
assert.NoError(t, err)
})
}
}
func TestGetOrderedResourcesForTypeTrimsSpaces(t *testing.T) {
// Spaces after commas are common in CLI input and should not break ordering.
orders := getOrderedResourcesForType(map[string]string{
"pods": "ns1/pod2, ns1/pod1",
}, "pods")
require.Equal(t, []string{"ns1/pod2", "ns1/pod1"}, orders)
log := logrus.StandardLogger()
podResources := []*kubernetesResource{
{namespace: "ns1", name: "pod3"},
{namespace: "ns1", name: "pod1"},
{namespace: "ns1", name: "pod2"},
}
sorted := sortResourcesByOrder(log, podResources, orders)
require.Equal(t, []*kubernetesResource{
{namespace: "ns1", name: "pod2", orderedResource: true},
{namespace: "ns1", name: "pod1", orderedResource: true},
{namespace: "ns1", name: "pod3"},
}, sorted)
}
func TestGetResourceItems_NamespacedFilterPolicies_ClusterWideListing(t *testing.T) {
// Simulate cluster-wide API calls where client is called with namespace=""
prodSA := unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": map[string]any{
"name": "default",
"namespace": "production",
},
},
}
defaultSA := unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": map[string]any{
"name": "default",
"namespace": "default",
},
},
}
kubeSystemSA := unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": map[string]any{
"name": "default",
"namespace": "kube-system",
},
},
}
prodCM1 := unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]any{
"name": "app-config",
"namespace": "production",
"labels": map[string]any{
"app": "frontend",
},
},
},
}
prodCM2 := unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]any{
"name": "other-config",
"namespace": "production",
"labels": map[string]any{
"app": "backend",
},
},
},
}
defaultCM := unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]any{
"name": "my-config",
"namespace": "default",
},
},
}
dcClusterWideSA := &test.FakeDynamicClient{}
dcClusterWideSA.On("List", mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{prodSA, defaultSA, kubeSystemSA}}, nil)
dcClusterWideCM := &test.FakeDynamicClient{}
dcClusterWideCM.On("List", mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{prodCM1, prodCM2, defaultCM}}, nil)
factory := &test.FakeDynamicFactory{}
factory.On("ClientForGroupVersionResource", schema.GroupVersion{Version: "v1"}, metav1.APIResource{Name: "serviceaccounts", Namespaced: true, Kind: "ServiceAccount"}, "").Return(dcClusterWideSA, nil)
factory.On("ClientForGroupVersionResource", schema.GroupVersion{Version: "v1"}, metav1.APIResource{Name: "configmaps", Namespaced: true, Kind: "ConfigMap"}, "").Return(dcClusterWideCM, nil)
frontendSelector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
MatchLabels: map[string]string{"app": "frontend"},
})
require.NoError(t, err)
req := &Request{
Backup: builder.ForBackup("velero", "backup").Result(),
NamespaceIncludesExcludes: collections.NewNamespaceIncludesExcludes().
Includes("*").
Excludes("kube-system"),
NamespacedFilterMap: map[string]*ResolvedNamespaceFilter{
"production": {
ResourceFilterMap: map[string]*ResolvedResourceFilter{
"configmaps": {
LabelSelector: frontendSelector,
},
},
},
},
ResourceIncludesExcludes: includeAllIE{},
}
tempDir := t.TempDir()
r := &itemCollector{
backupRequest: req,
dynamicFactory: factory,
discoveryHelper: test.NewFakeDiscoveryHelper(true, nil),
log: test.NewLogger(),
dir: tempDir,
}
// 1. ServiceAccounts:
// - production should be skipped because ServiceAccount is not in production's resourceFilters
// - kube-system should be skipped because kube-system is in excludedNamespaces
// - default should be included
saResource := metav1.APIResource{Name: "serviceaccounts", Namespaced: true, Kind: "ServiceAccount"}
saItems, err := r.getResourceItems(test.NewLogger(), schema.GroupVersion{Version: "v1"}, saResource, nil)
require.NoError(t, err)
require.Len(t, saItems, 1)
assert.Equal(t, "default", saItems[0].namespace)
assert.Equal(t, "default", saItems[0].name)
// 2. ConfigMaps:
// - production/app-config matches label app=frontend and should be included
// - production/other-config has label app=backend and should be skipped by labelSelector
// - default/my-config has no filter policy and should be included
cmResource := metav1.APIResource{Name: "configmaps", Namespaced: true, Kind: "ConfigMap"}
cmItems, err := r.getResourceItems(test.NewLogger(), schema.GroupVersion{Version: "v1"}, cmResource, nil)
require.NoError(t, err)
require.Len(t, cmItems, 2)
var cmNames []string
for _, it := range cmItems {
cmNames = append(cmNames, it.namespace+"/"+it.name)
}
assert.ElementsMatch(t, []string{"production/app-config", "default/my-config"}, cmNames)
}
func TestGetResourceItems_NamespacedFilterPolicies_SpecificNamespaces(t *testing.T) {
defaultSA := unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": map[string]any{
"name": "default",
"namespace": "default",
},
},
}
dcDefaultSA := &test.FakeDynamicClient{}
dcDefaultSA.On("List", mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{defaultSA}}, nil)
factory := &test.FakeDynamicFactory{}
// Note: production client is never even requested because production skips ServiceAccount at the loop top!
factory.On("ClientForGroupVersionResource", schema.GroupVersion{Version: "v1"}, metav1.APIResource{Name: "serviceaccounts", Namespaced: true, Kind: "ServiceAccount"}, "default").Return(dcDefaultSA, nil)
req := &Request{
Backup: builder.ForBackup("velero", "backup").Result(),
NamespaceIncludesExcludes: collections.NewNamespaceIncludesExcludes().
Includes("production", "default"),
NamespacedFilterMap: map[string]*ResolvedNamespaceFilter{
"production": {
ResourceFilterMap: map[string]*ResolvedResourceFilter{
"configmaps": {},
},
},
},
ResourceIncludesExcludes: includeAllIE{},
}
tempDir := t.TempDir()
r := &itemCollector{
backupRequest: req,
dynamicFactory: factory,
discoveryHelper: test.NewFakeDiscoveryHelper(true, nil),
log: test.NewLogger(),
dir: tempDir,
}
saResource := metav1.APIResource{Name: "serviceaccounts", Namespaced: true, Kind: "ServiceAccount"}
saItems, err := r.getResourceItems(test.NewLogger(), schema.GroupVersion{Version: "v1"}, saResource, nil)
require.NoError(t, err)
require.Len(t, saItems, 1)
assert.Equal(t, "default", saItems[0].namespace)
assert.Equal(t, "default", saItems[0].name)
}