mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-28 18:55:46 +00:00
Merge pull request #10275 from kaovilai/namespace-selection-by-label
Implement namespace selection by label in resource policy
This commit is contained in:
@@ -603,7 +603,12 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel
|
||||
// Empty IncludedNamespaces means "include all namespaces". Normalize
|
||||
// to ["*"] so that downstream wildcard expansion does not collapse
|
||||
// an empty-includes + wildcard-excludes combination into "back up nothing".
|
||||
if len(request.Spec.IncludedNamespaces) == 0 {
|
||||
// Recorded separately from the normalized value below: once normalized, an
|
||||
// originally-empty list and an explicitly-configured ["*"] are indistinguishable,
|
||||
// but mergeNamespacesByLabel's replace-vs-union decision needs to tell them apart
|
||||
// (see its doc comment).
|
||||
includedNamespacesWereDefaulted := len(request.Spec.IncludedNamespaces) == 0
|
||||
if includedNamespacesWereDefaulted {
|
||||
request.Spec.IncludedNamespaces = []string{"*"}
|
||||
}
|
||||
|
||||
@@ -636,10 +641,130 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, "include-resources, exclude-resources and include-cluster-resources are old filter parameters.\n"+
|
||||
"They cannot be used with namespace-scoped or fine-grained global filter policies.")
|
||||
}
|
||||
|
||||
// Resolve includedNamespacesByLabel/excludedNamespacesByLabel from the resource policy
|
||||
// (if configured) against the live namespace list, and merge into the effective
|
||||
// namespace filter. Must run after the velero.io/exclude-from-backup hard-exclusion
|
||||
// above, so that the "- BackupSpec.ExcludedNamespaces" term below already carries
|
||||
// hard-excluded namespaces.
|
||||
if resourcePolicies != nil && resourcePolicies.GetIncludeExcludePolicy() != nil {
|
||||
iep := resourcePolicies.GetIncludeExcludePolicy()
|
||||
if len(iep.IncludedNamespacesByLabel) > 0 || len(iep.ExcludedNamespacesByLabel) > 0 {
|
||||
resolvedIncluded, resolvedExcluded, err := resourcepolicies.ResolveNamespacesByLabel(
|
||||
ctx, b.kbClient,
|
||||
iep.IncludedNamespacesByLabel, iep.ExcludedNamespacesByLabel, iep.LabelSelectorLogic)
|
||||
if err != nil {
|
||||
request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("error resolving namespace label selectors: %v", err))
|
||||
} else {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"includedNamespacesByLabelCount": len(resolvedIncluded),
|
||||
"excludedNamespacesByLabelCount": len(resolvedExcluded),
|
||||
}).Info("resolved namespaces by label selector")
|
||||
logger.WithFields(logrus.Fields{
|
||||
"includedNamespacesByLabel": resolvedIncluded,
|
||||
"excludedNamespacesByLabel": resolvedExcluded,
|
||||
}).Debug("resolved namespaces by label selector detail")
|
||||
|
||||
request.Spec.IncludedNamespaces, request.Spec.ExcludedNamespaces = mergeNamespacesByLabel(
|
||||
request.Spec.IncludedNamespaces,
|
||||
request.Spec.ExcludedNamespaces,
|
||||
len(iep.IncludedNamespacesByLabel) > 0,
|
||||
includedNamespacesWereDefaulted,
|
||||
resolvedIncluded,
|
||||
resolvedExcluded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.ResPolicies = resourcePolicies
|
||||
return request
|
||||
}
|
||||
|
||||
// mergeNamespacesByLabel merges a resource policy's resolved includedNamespacesByLabel/
|
||||
// excludedNamespacesByLabel name sets into the backup's effective IncludedNamespaces/
|
||||
// ExcludedNamespaces, per the design's Precedence and Interaction rules:
|
||||
//
|
||||
// - includedNamespaces is assumed already normalized so that an originally-empty
|
||||
// BackupSpec.IncludedNamespaces reads as the ["*"] wildcard (prepareBackupRequest does
|
||||
// this normalization earlier, before resource-policy processing runs).
|
||||
// - includedNamespacesWereDefaulted reports whether that normalization actually fired -
|
||||
// i.e. whether BackupSpec.IncludedNamespaces was originally empty, as opposed to the user
|
||||
// having explicitly written ["*"] themselves. The two are indistinguishable by the time
|
||||
// includedNamespaces reaches this function (both read as ["*"]), so the caller must track
|
||||
// and pass this separately; inspecting includedNamespaces alone would wrongly narrow an
|
||||
// explicit ["*"] down to only the label matches instead of leaving it as "everything".
|
||||
// - labelIncludeActive reports whether includedNamespacesByLabel was *configured* at all
|
||||
// (not whether it matched anything - a configured selector matching zero namespaces must
|
||||
// still produce an empty-selection baseline, not fall through to "all namespaces").
|
||||
// - When labelIncludeActive and includedNamespacesWereDefaulted, resolvedIncluded REPLACES
|
||||
// the wildcard baseline (unioning into "all" would still be "all", defeating the feature's
|
||||
// primary use case of a schedule with no explicit includes) - represented via
|
||||
// resourcepolicies.RepresentNamespaceSelection so a zero-match result is expressed as a
|
||||
// wildcard pattern guaranteed to match nothing, not a plain empty slice (which
|
||||
// wildcard.ShouldExpandWildcards treats as "match everything" - see that function's doc
|
||||
// comment for why a bare empty list cannot be reused to mean the opposite here). When
|
||||
// explicit concrete names were already present, resolvedIncluded is unioned in additively
|
||||
// instead. When the user explicitly wrote ["*"] themselves (includedNamespacesWereDefaulted
|
||||
// is false but includedNamespaces is already ["*"]), unioning concrete names into it is a
|
||||
// no-op at match time (IncludesExcludes.ShouldInclude treats a "*" entry as match-everything
|
||||
// regardless of what else is in the list) but would also violate
|
||||
// collections.ValidateIncludesExcludes' "'*' must be alone in includes" invariant if the
|
||||
// merged result were ever re-validated - so this case canonicalizes back down to ["*"]
|
||||
// instead of widening it.
|
||||
// - resolvedExcluded is unioned into excludedNamespaces whenever non-empty, regardless of
|
||||
// labelIncludeActive - excludedNamespacesByLabel is purely subtractive, same role as
|
||||
// BackupSpec.ExcludedNamespaces today. An empty resolvedExcluded needs no such translation:
|
||||
// "exclude nothing" is unambiguous as a plain empty list, unlike "include nothing".
|
||||
// - Finally, unless mergedIncluded is the wildcard, any name present in both merged lists is
|
||||
// dropped from mergedIncluded (not mergedExcluded) so the two stay mutually exclusive.
|
||||
// Exclusion already wins over inclusion at match time regardless (same ShouldInclude
|
||||
// precedence as above), so this changes only the returned representation, not resolved
|
||||
// backup behavior - it keeps the merged lists satisfying
|
||||
// collections.ValidateIncludesExcludes' "excludes list cannot contain an item in the
|
||||
// includes list" invariant too, for the same reason the "*" case above is canonicalized
|
||||
// rather than left as an invariant-violating pair.
|
||||
func mergeNamespacesByLabel(
|
||||
includedNamespaces []string,
|
||||
excludedNamespaces []string,
|
||||
labelIncludeActive bool,
|
||||
includedNamespacesWereDefaulted bool,
|
||||
resolvedIncluded []string,
|
||||
resolvedExcluded []string,
|
||||
) (mergedIncluded []string, mergedExcluded []string) {
|
||||
mergedIncluded = includedNamespaces
|
||||
if labelIncludeActive {
|
||||
switch {
|
||||
case includedNamespacesWereDefaulted:
|
||||
mergedIncluded = resourcepolicies.RepresentNamespaceSelection(resolvedIncluded)
|
||||
case sets.NewString(includedNamespaces...).Has("*"):
|
||||
mergedIncluded = []string{"*"}
|
||||
default:
|
||||
mergedIncluded = sets.NewString(includedNamespaces...).Insert(resolvedIncluded...).List()
|
||||
}
|
||||
}
|
||||
|
||||
mergedExcluded = excludedNamespaces
|
||||
if len(resolvedExcluded) > 0 {
|
||||
mergedExcluded = sets.NewString(excludedNamespaces...).Insert(resolvedExcluded...).List()
|
||||
}
|
||||
|
||||
if !sets.NewString(mergedIncluded...).Has("*") {
|
||||
// Difference can legitimately empty this out entirely (every resolved or explicit
|
||||
// include also landed in mergedExcluded) - route back through
|
||||
// RepresentNamespaceSelection so that comes back as the no-match sentinel, not a bare
|
||||
// empty slice. The same "empty means include everything" hazard that motivated the
|
||||
// zero-match sentinel above applies here too: an empty result at this point means
|
||||
// "everything that was included is now excluded", i.e. include nothing, and a plain
|
||||
// empty []string would be silently reinterpreted downstream as the opposite.
|
||||
mergedIncluded = resourcepolicies.RepresentNamespaceSelection(
|
||||
sets.NewString(mergedIncluded...).Difference(sets.NewString(mergedExcluded...)).List(),
|
||||
)
|
||||
}
|
||||
|
||||
return mergedIncluded, mergedExcluded
|
||||
}
|
||||
|
||||
// validateAndGetSnapshotLocations gets a collection of VolumeSnapshotLocation objects that
|
||||
// this backup will use (returned as a map of provider name -> VSL), and ensures:
|
||||
// - each location name in .spec.volumeSnapshotLocations exists as a location
|
||||
|
||||
@@ -354,6 +354,417 @@ func TestPrepareBackupRequest_EmptyIncludedNamespacesNormalizedToWildcard(t *tes
|
||||
assert.Equal(t, []string{"*"}, res.Spec.IncludedNamespaces)
|
||||
}
|
||||
|
||||
// TestPrepareBackupRequest_IncludedNamespacesByLabel_ReplacesWildcardBaseline verifies that
|
||||
// when includedNamespacesByLabel is configured and BackupSpec.IncludedNamespaces was left
|
||||
// empty (normalized to the ["*"] wildcard), the label-resolved namespace set REPLACES the
|
||||
// wildcard baseline rather than being unioned into it - otherwise "all" unioned with anything
|
||||
// is still "all", defeating the feature.
|
||||
func TestPrepareBackupRequest_IncludedNamespacesByLabel_ReplacesWildcardBaseline(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
|
||||
policyYAML := `version: v1
|
||||
includeExcludePolicy:
|
||||
includedNamespacesByLabel:
|
||||
- "team=platform"
|
||||
`
|
||||
policyConfigMap := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "ns-label-policy", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: map[string]string{"policy": policyYAML},
|
||||
}
|
||||
|
||||
backupLocation := builder.ForBackupStorageLocation("velero", "loc-1").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
nsPlatform := builder.ForNamespace("platform-ns").ObjectMeta(builder.WithLabels("team", "platform")).Result()
|
||||
nsOther := builder.ForNamespace("other-ns").ObjectMeta(builder.WithLabels("team", "infra")).Result()
|
||||
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, policyConfigMap, backupLocation, nsPlatform, nsOther)
|
||||
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := &backupReconciler{
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
defaultBackupLocation: backupLocation.Name,
|
||||
clock: &clock.RealClock{},
|
||||
formatFlag: formatFlag,
|
||||
}
|
||||
|
||||
backup := defaultBackup().Result()
|
||||
backup.Spec.IncludedNamespaces = nil
|
||||
backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{Kind: "configmap", Name: "ns-label-policy"}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, backup, logger)
|
||||
defer res.WorkerPool.Stop()
|
||||
|
||||
assert.Empty(t, res.Status.ValidationErrors)
|
||||
assert.Equal(t, []string{"platform-ns"}, res.Spec.IncludedNamespaces)
|
||||
}
|
||||
|
||||
// TestPrepareBackupRequest_IncludedNamespacesByLabel_UnionsWithExplicitIncludes verifies that
|
||||
// when BackupSpec.IncludedNamespaces already has explicit names, the label-resolved set is
|
||||
// additive (unioned), not a replacement.
|
||||
func TestPrepareBackupRequest_IncludedNamespacesByLabel_UnionsWithExplicitIncludes(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
|
||||
policyYAML := `version: v1
|
||||
includeExcludePolicy:
|
||||
includedNamespacesByLabel:
|
||||
- "team=platform"
|
||||
`
|
||||
policyConfigMap := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "ns-label-policy", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: map[string]string{"policy": policyYAML},
|
||||
}
|
||||
|
||||
backupLocation := builder.ForBackupStorageLocation("velero", "loc-1").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
nsPlatform := builder.ForNamespace("platform-ns").ObjectMeta(builder.WithLabels("team", "platform")).Result()
|
||||
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, policyConfigMap, backupLocation, nsPlatform)
|
||||
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := &backupReconciler{
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
defaultBackupLocation: backupLocation.Name,
|
||||
clock: &clock.RealClock{},
|
||||
formatFlag: formatFlag,
|
||||
}
|
||||
|
||||
backup := defaultBackup().IncludedNamespaces("explicit-ns").Result()
|
||||
backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{Kind: "configmap", Name: "ns-label-policy"}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, backup, logger)
|
||||
defer res.WorkerPool.Stop()
|
||||
|
||||
assert.Empty(t, res.Status.ValidationErrors)
|
||||
assert.ElementsMatch(t, []string{"explicit-ns", "platform-ns"}, res.Spec.IncludedNamespaces)
|
||||
}
|
||||
|
||||
// TestPrepareBackupRequest_IncludedNamespacesByLabel_ZeroMatchesResolvesToNoMatchSentinel
|
||||
// verifies that a configured includedNamespacesByLabel selector matching zero namespaces
|
||||
// resolves to resourcepolicies.NoNamespaceMatchesPattern, not a fall-through to "all
|
||||
// namespaces" - this is deliberate fail-safe behavior, not a bug.
|
||||
func TestPrepareBackupRequest_IncludedNamespacesByLabel_ZeroMatchesResolvesToNoMatchSentinel(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
|
||||
policyYAML := `version: v1
|
||||
includeExcludePolicy:
|
||||
includedNamespacesByLabel:
|
||||
- "team=nonexistent"
|
||||
`
|
||||
policyConfigMap := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "ns-label-policy", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: map[string]string{"policy": policyYAML},
|
||||
}
|
||||
|
||||
backupLocation := builder.ForBackupStorageLocation("velero", "loc-1").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
nsOther := builder.ForNamespace("other-ns").ObjectMeta(builder.WithLabels("team", "infra")).Result()
|
||||
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, policyConfigMap, backupLocation, nsOther)
|
||||
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := &backupReconciler{
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
defaultBackupLocation: backupLocation.Name,
|
||||
clock: &clock.RealClock{},
|
||||
formatFlag: formatFlag,
|
||||
}
|
||||
|
||||
backup := defaultBackup().Result()
|
||||
backup.Spec.IncludedNamespaces = nil
|
||||
backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{Kind: "configmap", Name: "ns-label-policy"}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, backup, logger)
|
||||
defer res.WorkerPool.Stop()
|
||||
|
||||
assert.Empty(t, res.Status.ValidationErrors)
|
||||
// Not a plain empty slice - see resourcepolicies.NoNamespaceMatchesPattern's doc comment
|
||||
// for why a bare empty list here would be silently reinterpreted downstream as "include
|
||||
// everything" instead of the intended "include nothing".
|
||||
assert.Equal(t, []string{resourcepolicies.NoNamespaceMatchesPattern}, res.Spec.IncludedNamespaces)
|
||||
}
|
||||
|
||||
// TestPrepareBackupRequest_IncludedNamespacesByLabel_ExplicitWildcardCanonicalized verifies
|
||||
// that when BackupSpec.IncludedNamespaces was explicitly set to ["*"] (as opposed to left
|
||||
// empty and normalized to ["*"]), includedNamespacesByLabel does not narrow the backup down
|
||||
// to only the label matches - the two must not be conflated (see mergeNamespacesByLabel's doc
|
||||
// comment). The result stays canonicalized to ["*"] rather than widened to ["*", "platform-ns"]:
|
||||
// both are equivalent at match time, but only the former satisfies
|
||||
// collections.ValidateIncludesExcludes' "'*' must be alone in includes" invariant.
|
||||
func TestPrepareBackupRequest_IncludedNamespacesByLabel_ExplicitWildcardCanonicalized(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
|
||||
policyYAML := `version: v1
|
||||
includeExcludePolicy:
|
||||
includedNamespacesByLabel:
|
||||
- "team=platform"
|
||||
`
|
||||
policyConfigMap := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "ns-label-policy", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: map[string]string{"policy": policyYAML},
|
||||
}
|
||||
|
||||
backupLocation := builder.ForBackupStorageLocation("velero", "loc-1").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
nsPlatform := builder.ForNamespace("platform-ns").ObjectMeta(builder.WithLabels("team", "platform")).Result()
|
||||
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, policyConfigMap, backupLocation, nsPlatform)
|
||||
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := &backupReconciler{
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
defaultBackupLocation: backupLocation.Name,
|
||||
clock: &clock.RealClock{},
|
||||
formatFlag: formatFlag,
|
||||
}
|
||||
|
||||
backup := defaultBackup().IncludedNamespaces("*").Result()
|
||||
backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{Kind: "configmap", Name: "ns-label-policy"}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, backup, logger)
|
||||
defer res.WorkerPool.Stop()
|
||||
|
||||
assert.Empty(t, res.Status.ValidationErrors)
|
||||
assert.ElementsMatch(t, []string{"*"}, res.Spec.IncludedNamespaces)
|
||||
}
|
||||
|
||||
// TestPrepareBackupRequest_ExcludedNamespacesByLabel_Subtracted verifies excludedNamespacesByLabel
|
||||
// resolves independently and is merged into BackupSpec.ExcludedNamespaces, regardless of whether
|
||||
// includedNamespacesByLabel is configured.
|
||||
func TestPrepareBackupRequest_ExcludedNamespacesByLabel_Subtracted(t *testing.T) {
|
||||
formatFlag := logging.FormatText
|
||||
logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag)
|
||||
|
||||
policyYAML := `version: v1
|
||||
includeExcludePolicy:
|
||||
excludedNamespacesByLabel:
|
||||
- "confidential=true"
|
||||
`
|
||||
policyConfigMap := &corev1api.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "ns-label-policy", Namespace: velerov1api.DefaultNamespace},
|
||||
Data: map[string]string{"policy": policyYAML},
|
||||
}
|
||||
|
||||
backupLocation := builder.ForBackupStorageLocation("velero", "loc-1").Phase(velerov1api.BackupStorageLocationPhaseAvailable).Result()
|
||||
nsConfidential := builder.ForNamespace("secret-ns").ObjectMeta(builder.WithLabels("confidential", "true")).Result()
|
||||
|
||||
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, policyConfigMap, backupLocation, nsConfidential)
|
||||
|
||||
apiServer := velerotest.NewAPIServer(t)
|
||||
discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := &backupReconciler{
|
||||
discoveryHelper: discoveryHelper,
|
||||
kbClient: fakeClient,
|
||||
defaultBackupLocation: backupLocation.Name,
|
||||
clock: &clock.RealClock{},
|
||||
formatFlag: formatFlag,
|
||||
}
|
||||
|
||||
backup := defaultBackup().Result()
|
||||
backup.Spec.IncludedNamespaces = nil
|
||||
backup.Spec.ResourcePolicy = &corev1api.TypedLocalObjectReference{Kind: "configmap", Name: "ns-label-policy"}
|
||||
|
||||
res := c.prepareBackupRequest(ctx, backup, logger)
|
||||
defer res.WorkerPool.Stop()
|
||||
|
||||
assert.Empty(t, res.Status.ValidationErrors)
|
||||
// includedNamespacesByLabel not configured, so baseline stays the wildcard.
|
||||
assert.Equal(t, []string{"*"}, res.Spec.IncludedNamespaces)
|
||||
assert.Equal(t, []string{"secret-ns"}, res.Spec.ExcludedNamespaces)
|
||||
}
|
||||
|
||||
// TestMergeNamespacesByLabel exercises the union/replacement decision directly, without
|
||||
// the full prepareBackupRequest scaffold (fake client, discovery helper, resource policy
|
||||
// ConfigMap, etc.) - see mergeNamespacesByLabel's doc comment for the precedence rules.
|
||||
func TestMergeNamespacesByLabel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
includedNamespaces []string
|
||||
excludedNamespaces []string
|
||||
labelIncludeActive bool
|
||||
includedNamespacesWereDefaulted bool
|
||||
resolvedIncluded []string
|
||||
resolvedExcluded []string
|
||||
wantIncluded []string
|
||||
wantExcluded []string
|
||||
}{
|
||||
{
|
||||
name: "defaulted wildcard baseline is replaced",
|
||||
includedNamespaces: []string{"*"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: true,
|
||||
resolvedIncluded: []string{"platform-ns"},
|
||||
wantIncluded: []string{"platform-ns"},
|
||||
wantExcluded: nil,
|
||||
},
|
||||
{
|
||||
name: "explicit includes union additively",
|
||||
includedNamespaces: []string{"explicit-ns"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: false,
|
||||
resolvedIncluded: []string{"platform-ns"},
|
||||
wantIncluded: []string{"explicit-ns", "platform-ns"},
|
||||
wantExcluded: nil,
|
||||
},
|
||||
{
|
||||
// Explicit includes can themselves be a non-"*" wildcard glob (e.g. from
|
||||
// --include-namespaces 'app-*'), not just concrete names - the union branch must
|
||||
// pass that through unchanged alongside the resolved concrete names; downstream
|
||||
// wildcard.ShouldExpandWildcards still expands it normally since it isn't the bare
|
||||
// "*" special case.
|
||||
name: "explicit glob-pattern include unions with resolved names unchanged",
|
||||
includedNamespaces: []string{"app-*"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: false,
|
||||
resolvedIncluded: []string{"platform-ns"},
|
||||
wantIncluded: []string{"app-*", "platform-ns"},
|
||||
wantExcluded: nil,
|
||||
},
|
||||
{
|
||||
// A bare empty []string here would be interpreted downstream by
|
||||
// wildcard.ShouldExpandWildcards as "match everything" (its own documented
|
||||
// behavior), silently defeating the fail-safe. Must come back as
|
||||
// resourcepolicies.NoNamespaceMatchesPattern instead - see
|
||||
// mergeNamespacesByLabel's doc comment.
|
||||
name: "defaulted wildcard matching zero namespaces resolves to the no-match sentinel, not empty",
|
||||
includedNamespaces: []string{"*"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: true,
|
||||
resolvedIncluded: nil,
|
||||
wantIncluded: []string{resourcepolicies.NoNamespaceMatchesPattern},
|
||||
wantExcluded: nil,
|
||||
},
|
||||
{
|
||||
// The code must not re-derive "was this defaulted" by checking
|
||||
// includedNamespaces == ["*"], since an explicitly-configured wildcard looks
|
||||
// identical to the normalized default by this point. An explicit ["*"] must stay
|
||||
// "everything", never narrow to just the label matches - canonicalized back to
|
||||
// ["*"] rather than widened to ["*", "platform-ns"], since the latter is
|
||||
// semantically identical at match time but would violate
|
||||
// collections.ValidateIncludesExcludes' "'*' must be alone in includes" invariant.
|
||||
name: "explicitly-configured wildcard canonicalizes to itself instead of widening",
|
||||
includedNamespaces: []string{"*"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: false,
|
||||
resolvedIncluded: []string{"platform-ns"},
|
||||
wantIncluded: []string{"*"},
|
||||
wantExcluded: nil,
|
||||
},
|
||||
{
|
||||
name: "explicitly-configured wildcard stays everything even on zero matches",
|
||||
includedNamespaces: []string{"*"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: false,
|
||||
resolvedIncluded: nil,
|
||||
wantIncluded: []string{"*"},
|
||||
wantExcluded: nil,
|
||||
},
|
||||
{
|
||||
name: "not labelIncludeActive leaves includedNamespaces untouched",
|
||||
includedNamespaces: []string{"*"},
|
||||
labelIncludeActive: false,
|
||||
includedNamespacesWereDefaulted: true,
|
||||
resolvedIncluded: []string{"platform-ns"}, // should be ignored
|
||||
wantIncluded: []string{"*"},
|
||||
wantExcluded: nil,
|
||||
},
|
||||
{
|
||||
name: "resolvedExcluded unions in regardless of labelIncludeActive",
|
||||
includedNamespaces: []string{"*"},
|
||||
excludedNamespaces: []string{"legacy-ns"},
|
||||
labelIncludeActive: false,
|
||||
includedNamespacesWereDefaulted: true,
|
||||
resolvedExcluded: []string{"secret-ns"},
|
||||
wantIncluded: []string{"*"},
|
||||
wantExcluded: []string{"legacy-ns", "secret-ns"},
|
||||
},
|
||||
{
|
||||
name: "empty resolvedExcluded leaves excludedNamespaces untouched",
|
||||
includedNamespaces: []string{"*"},
|
||||
excludedNamespaces: []string{"legacy-ns"},
|
||||
labelIncludeActive: false,
|
||||
includedNamespacesWereDefaulted: true,
|
||||
resolvedExcluded: nil,
|
||||
wantIncluded: []string{"*"},
|
||||
wantExcluded: []string{"legacy-ns"},
|
||||
},
|
||||
{
|
||||
// A resolved-include name overlapping an already-excluded name violates
|
||||
// collections.ValidateIncludesExcludes' "excludes list cannot contain an item in
|
||||
// the includes list" invariant if the merged result were ever re-validated.
|
||||
// Exclusion already wins at match time regardless (IncludesExcludes.ShouldInclude
|
||||
// checks excludes first), so dropping the overlap from mergedIncluded changes only
|
||||
// the returned representation, not resolved backup behavior.
|
||||
name: "a resolved include overlapping an existing exclude is dropped from the merged includes",
|
||||
includedNamespaces: []string{"explicit-ns"},
|
||||
excludedNamespaces: []string{"both-ns"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: false,
|
||||
resolvedIncluded: []string{"both-ns", "platform-ns"},
|
||||
wantIncluded: []string{"explicit-ns", "platform-ns"},
|
||||
wantExcluded: []string{"both-ns"},
|
||||
},
|
||||
{
|
||||
// The exclude-subtraction step itself can produce a bare empty []string when
|
||||
// every explicit include is also excluded - the same "empty means include
|
||||
// everything" hazard the zero-match sentinel exists for, reached through a
|
||||
// different path.
|
||||
name: "an explicit include fully removed by exclusion resolves to the no-match sentinel, not empty",
|
||||
includedNamespaces: []string{"explicit-ns"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: false,
|
||||
resolvedExcluded: []string{"explicit-ns"},
|
||||
wantIncluded: []string{resourcepolicies.NoNamespaceMatchesPattern},
|
||||
wantExcluded: []string{"explicit-ns"},
|
||||
},
|
||||
{
|
||||
// Same hazard, but for a defaulted-wildcard-replaced resolved include (rather
|
||||
// than an explicit one) that a same-namespace excludedNamespacesByLabel match
|
||||
// fully removes.
|
||||
name: "a resolved include fully removed by exclusion resolves to the no-match sentinel, not empty",
|
||||
includedNamespaces: []string{"*"},
|
||||
labelIncludeActive: true,
|
||||
includedNamespacesWereDefaulted: true,
|
||||
resolvedIncluded: []string{"both-ns"},
|
||||
resolvedExcluded: []string{"both-ns"},
|
||||
wantIncluded: []string{resourcepolicies.NoNamespaceMatchesPattern},
|
||||
wantExcluded: []string{"both-ns"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotIncluded, gotExcluded := mergeNamespacesByLabel(
|
||||
tc.includedNamespaces,
|
||||
tc.excludedNamespaces,
|
||||
tc.labelIncludeActive,
|
||||
tc.includedNamespacesWereDefaulted,
|
||||
tc.resolvedIncluded,
|
||||
tc.resolvedExcluded,
|
||||
)
|
||||
assert.ElementsMatch(t, tc.wantIncluded, gotIncluded)
|
||||
assert.ElementsMatch(t, tc.wantExcluded, gotExcluded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_prepareBackupRequest_BackupStorageLocation(t *testing.T) {
|
||||
var (
|
||||
defaultBackupTTL = metav1.Duration{Duration: 24 * 30 * time.Hour}
|
||||
|
||||
Reference in New Issue
Block a user