mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-17 21:44:23 +00:00
Design: namespace selection by label in resource policy (#9772)
Run the E2E test on kind / setup-test-matrix (push) Failing after 3s
Scorecard supply-chain security / Scorecard analysis (push) Skipped
e2e-test-kind.yaml / extract (push) Failing after 5s
Run the E2E test on kind / get-go-version (push) Failing after 7s
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
Run the E2E test on kind / setup-test-matrix (push) Failing after 3s
Scorecard supply-chain security / Scorecard analysis (push) Skipped
e2e-test-kind.yaml / extract (push) Failing after 5s
Run the E2E test on kind / get-go-version (push) Failing after 7s
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
* Design: namespace selection by label in resource policy Extends includeExcludePolicy in the ResourcePolicy ConfigMap with includedNamespacesByLabel and excludedNamespacesByLabel, letting users dynamically include/exclude namespaces by Kubernetes label selector without touching BackupSpec or schedule specs. Covers precedence rules against BackupSpec.IncludedNamespaces/ ExcludedNamespaces (including the empty-IncludedNamespaces "all namespaces" baseline and a configured selector matching zero namespaces), observability via logging (backup.status.includedNamespaces is deferred, see Open Issues), and rejected alternatives (extending BackupSpec.LabelSelector, a new CRD field, a standalone ConfigMap, matching the fine-grained filters' map[string]string selector shape). resolveNamespacesByLabel resolves included/excluded selectors independently and returns both sets uncombined; the caller merges them against BackupSpec.IncludedNamespaces/ExcludedNamespaces via an explicit labelIncludeActive flag, so exclude-only policies and zero-match include selectors both behave correctly instead of collapsing to "all namespaces". Cross-checked against the fine-grained backup/restore filter policies merged from issue #9448 (clusterScopedFilterPolicy, namespacedFilterPolicies in internal/resourcepolicies/resource_policies.go): confirms no field/key collisions, documents that a ConfigMap using these fields can't be reused for Restore (ValidateForRestore rejects any non-nil IncludeExcludePolicy) or the global backup volume policies ConfigMap (volumePolicies-only). Fixes #9771 > [!Note] > Responses generated with Claude Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> * Design: address review feedback from adam-jian-zhang - Add markdown hard-line-break trailing spaces to Glossary entries so each term renders on its own line. - Add labelSelectorLogic ("OR" default | "AND") to IncludeExcludePolicy instead of deferring AND-vs-OR to a future enhancement, per review comment. AND combines entries within includedNamespacesByLabel (and independently within excludedNamespacesByLabel) via intersection instead of union. Threaded through resolveNamespacesByLabel, validation, and a new worked example. - Change observability logging from "log the full resolved namespace list at info" to "log the count at info, full list at debug" to avoid spamming backup logs on clusters with large namespace counts where a selector matches a large fraction of them. > [!Note] > Responses generated with Claude Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> * Design: clarify LabelSelector interaction with label-resolved namespaces Non-Goals previously asserted BackupSpec.LabelSelector/OrLabelSelectors are unaffected by this design without qualification. Add the missing trade-off: a namespace resolved into the effective set via includedNamespacesByLabel still gets its Namespace object written even if it doesn't separately match LabelSelector/OrLabelSelectors, mirroring existing behavior for namespaces named explicitly in BackupSpec.IncludedNamespaces (the nsTracker guard in pkg/backup/item_collector.go only suppresses the Namespace object when the namespace filter is at its default, per #7105). Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> --------- Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
# Namespace Selection by Label in Resource Policy
|
||||
|
||||
## Glossary & Abbreviation
|
||||
|
||||
**Backup Filter**: The mechanism in Velero that determines which Kubernetes resources are collected from the cluster and written into the backup archive. Backup filters currently operate on four dimensions: namespace, resource type, label, and cluster scope.
|
||||
**Global Filter**: A filter that applies uniformly across all namespaces in a backup. All existing Velero backup filters are global filters.
|
||||
**Namespace Label Selector Filter**: A filter that dynamically includes or excludes entire namespaces from a backup based on Kubernetes label selectors applied to namespace objects. This is the capability introduced by this design.
|
||||
**Resource Policy**: An existing Velero mechanism where backup behavior rules are defined in a ConfigMap and referenced from `BackupSpec.ResourcePolicy`. Currently used for volume policies and global include/exclude policies.
|
||||
|
||||
## Abstract
|
||||
|
||||
This proposal extends Velero's `includeExcludePolicy` in the ResourcePolicy ConfigMap to support selecting namespaces by Kubernetes label selectors.
|
||||
Users can dynamically include or exclude namespaces from backups without modifying `BackupSpec` or schedule specs.
|
||||
|
||||
## Background
|
||||
|
||||
Velero's backup filter system allows users to specify which resources to include or exclude from a backup. The filters operate on four dimensions:
|
||||
|
||||
1. **Namespace** — `IncludedNamespaces`/`ExcludedNamespaces` select which namespaces to back up
|
||||
2. **Resource Type** — `IncludedResources`/`ExcludedResources` (or the newer scoped variants `Included/ExcludedClusterScopedResources`, `Included/ExcludedNamespaceScopedResources`) select which Kubernetes resource types to back up
|
||||
3. **Labels** — `LabelSelector`/`OrLabelSelectors` filter individual objects by their labels
|
||||
4. **Cluster Scope** — `IncludeClusterResources` controls whether cluster-scoped resources are included
|
||||
|
||||
All four dimensions are applied **globally** — the same filters apply uniformly throughout the entire backup operation. There is no mechanism to dynamically resolve which namespaces to include based on namespace labels.
|
||||
|
||||
Users managing many clusters or dynamic namespace sets need a way to declare "back up namespaces labeled `backup=weekly`" without enumerating names.
|
||||
Issue [#7492](https://github.com/vmware-tanzu/velero/issues/7492) originally proposed extending `--selector` on the Backup, but the community preferred not to overload existing backup filters.
|
||||
The agreed direction is to model namespace selection by label as a Resource Policy capability, extending `includeExcludePolicy` rather than adding new fields to `BackupSpec`.
|
||||
|
||||
Separately, issue [#9448](https://github.com/velero-io/velero/issues/9448) drove a related but distinct extension of the same ResourcePolicy ConfigMap: per-namespace, per-kind resource filtering, merged as `clusterScopedFilterPolicy` and `namespacedFilterPolicies` (see [Fine Grained Backup Filters](backup-filter-enhancement/fine-grained-backup-filters-design.md) and [Fine Grained Restore Filters](restore-filter-enhancement/fine-grained-restore-filters-design.md)). That work decides *which resources* are collected once a namespace is already selected; this design decides *which namespaces* are selected in the first place. The two live under different top-level ConfigMap keys and share no fields — see [Detailed Design](#detailed-design) for confirmation this design remains compatible with the merged implementation.
|
||||
|
||||
The existing `includeExcludePolicy` already holds reusable include/exclude resource filters.
|
||||
Adding namespace label selectors here is consistent with its purpose and avoids spec sprawl.
|
||||
|
||||
## Goals
|
||||
|
||||
- Allow users to specify Kubernetes label selectors in `includeExcludePolicy` to dynamically include or exclude namespaces in a backup.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Changing the behavior of `BackupSpec.LabelSelector` or `BackupSpec.OrLabelSelectors` (those continue to filter individual resources, not namespaces — see [Precedence and Interaction](#precedence-and-interaction) for how this interacts with `includedNamespacesByLabel` specifically).
|
||||
- Selecting cluster-scoped resources by label (a separate concern; see [Fine Grained Backup Filters](backup-filter-enhancement/fine-grained-backup-filters-design.md), merged via `clusterScopedFilterPolicy`, for cluster-scoped resource filtering).
|
||||
- Selecting individual namespaced resources by namespace label (resources must still be individually matched).
|
||||
- Changing existing `BackupSpec` fields (`IncludedResources`, `LabelSelector`, etc.) or adding new CRD fields is explicitly avoided by this design.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Dynamic per-schedule namespace targeting
|
||||
|
||||
A user defines daily and weekly schedules.
|
||||
Namespace owners opt their namespace into a schedule by adding a label.
|
||||
The schedules never need updating as namespaces are added or removed.
|
||||
|
||||
```yaml
|
||||
# resource-policy-weekly.yaml (ConfigMap data)
|
||||
version: v1
|
||||
includeExcludePolicy:
|
||||
includedNamespacesByLabel:
|
||||
- "velero-backup-schedule=weekly"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# resource-policy-daily.yaml
|
||||
version: v1
|
||||
includeExcludePolicy:
|
||||
includedNamespacesByLabel:
|
||||
- "velero-backup-schedule=daily"
|
||||
```
|
||||
|
||||
### Exclude sensitive namespaces by label
|
||||
|
||||
A cluster operator excludes any namespace labeled `confidential=true` from all backups.
|
||||
|
||||
```yaml
|
||||
version: v1
|
||||
includeExcludePolicy:
|
||||
excludedNamespacesByLabel:
|
||||
- "confidential=true"
|
||||
```
|
||||
|
||||
### Combined include and exclude with multiple selectors
|
||||
|
||||
Include all namespaces labeled `team=platform` OR `team=infra`, but exclude those also labeled `env=dev`.
|
||||
|
||||
```yaml
|
||||
version: v1
|
||||
includeExcludePolicy:
|
||||
includedNamespacesByLabel:
|
||||
- "team=platform"
|
||||
- "team=infra"
|
||||
excludedNamespacesByLabel:
|
||||
- "env=dev"
|
||||
```
|
||||
|
||||
### Require multiple labels together (AND)
|
||||
|
||||
Only back up namespaces that are labeled both `tier=critical` and `compliance=pci`, using `labelSelectorLogic: "AND"` instead of the default OR:
|
||||
|
||||
```yaml
|
||||
version: v1
|
||||
includeExcludePolicy:
|
||||
labelSelectorLogic: "AND"
|
||||
includedNamespacesByLabel:
|
||||
- "tier=critical"
|
||||
- "compliance=pci"
|
||||
```
|
||||
|
||||
Without `labelSelectorLogic: "AND"`, the two entries above would be OR'd (namespaces matching either label), which is a materially different — and in this case incorrect — selection. Note `"tier=critical,compliance=pci"` as a single comma-separated selector string already expresses AND today (comma-separated requirements within one selector string are always AND'd by `labels.Parse`); `labelSelectorLogic: "AND"` is for AND'ing across separate list *entries*, useful when entries are generated/templated independently rather than authored as one string.
|
||||
|
||||
## High-Level Design
|
||||
|
||||
Two new fields, `includedNamespacesByLabel` and `excludedNamespacesByLabel`, are added to `IncludeExcludePolicy` in the ResourcePolicy ConfigMap.
|
||||
Each field is a list of Kubernetes label selector strings (same syntax as `kubectl get ns -l`).
|
||||
|
||||
At backup time, Velero evaluates each selector against the live namespace list. If any `includedNamespacesByLabel` selector is configured, the resolved union becomes the inclusion baseline (additive with an explicit, non-empty `BackupSpec.IncludedNamespaces`); otherwise the existing "empty means all namespaces" baseline is unchanged. Namespaces matched by any `excludedNamespacesByLabel` selector are then subtracted from that baseline, same as `BackupSpec.ExcludedNamespaces` today. See [Precedence and Interaction](#precedence-and-interaction) for the exact merge order. The resolved set is logged so operators can observe which namespaces were actually selected (see [Observability](#observability)).
|
||||
|
||||
This design coexists with the existing `includeExcludePolicy` fields (`includedClusterScopedResources`, `excludedClusterScopedResources`, `includedNamespaceScopedResources`, `excludedNamespaceScopedResources`) and is independent of the `namespacedFilterPolicies` and `clusterScopedFilterPolicy` top-level `ResourcePolicies` keys, now merged (`internal/resourcepolicies/resource_policies.go`) per the [Fine Grained Backup Filters](backup-filter-enhancement/fine-grained-backup-filters-design.md) design.
|
||||
|
||||
## Detailed Design
|
||||
|
||||
### Data Structure
|
||||
|
||||
`IncludeExcludePolicy` in `internal/resourcepolicies/resource_policies.go` gains three new fields:
|
||||
|
||||
```go
|
||||
type IncludeExcludePolicy struct {
|
||||
IncludedClusterScopedResources []string `yaml:"includedClusterScopedResources"`
|
||||
ExcludedClusterScopedResources []string `yaml:"excludedClusterScopedResources"`
|
||||
IncludedNamespaceScopedResources []string `yaml:"includedNamespaceScopedResources"`
|
||||
ExcludedNamespaceScopedResources []string `yaml:"excludedNamespaceScopedResources"`
|
||||
// New fields
|
||||
IncludedNamespacesByLabel []string `yaml:"includedNamespacesByLabel"`
|
||||
ExcludedNamespacesByLabel []string `yaml:"excludedNamespacesByLabel"`
|
||||
// LabelSelectorLogic controls how multiple entries within
|
||||
// IncludedNamespacesByLabel are combined with each other, and
|
||||
// independently how multiple entries within ExcludedNamespacesByLabel
|
||||
// are combined with each other. "OR" (default) or "AND". Empty string
|
||||
// is treated as "OR".
|
||||
LabelSelectorLogic string `yaml:"labelSelectorLogic,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
Each entry in `includedNamespacesByLabel` / `excludedNamespacesByLabel` is a label selector string parseable by `k8s.io/apimachinery/pkg/labels.Parse`.
|
||||
By default (`labelSelectorLogic` unset or `"OR"`), multiple entries within the same list are combined with OR (union): a namespace matching any selector in the list is included/excluded. Setting `labelSelectorLogic: "AND"` combines entries within each list with AND (intersection) instead: a namespace must match every selector in `includedNamespacesByLabel` to be included, and every selector in `excludedNamespacesByLabel` to be excluded. `includedNamespacesByLabel` and `excludedNamespacesByLabel` are still resolved independently of each other regardless of `labelSelectorLogic` — see [Namespace Resolution](#namespace-resolution).
|
||||
Within a single selector string, comma-separated requirements are always AND (this is `labels.Parse` syntax, unrelated to `labelSelectorLogic`).
|
||||
|
||||
Example YAML in ResourcePolicy ConfigMap:
|
||||
|
||||
```yaml
|
||||
version: v1
|
||||
includeExcludePolicy:
|
||||
includedNamespacesByLabel:
|
||||
- "team=platform,env=prod" # namespaces with BOTH labels
|
||||
- "team=infra" # OR namespaces with this label
|
||||
excludedNamespacesByLabel:
|
||||
- "skip-backup=true"
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
`IncludeExcludePolicy.Validate()` is extended to parse each selector string and return an error if any is invalid:
|
||||
|
||||
```go
|
||||
func validateLabelSelectors(selectors []string) error {
|
||||
for _, s := range selectors {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return fmt.Errorf("label selector cannot be empty")
|
||||
}
|
||||
if _, err := labels.Parse(s); err != nil {
|
||||
return fmt.Errorf("invalid label selector %q: %w", s, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLabelSelectorLogic(logic string) error {
|
||||
switch logic {
|
||||
case "", "OR", "AND":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("labelSelectorLogic must be \"OR\" or \"AND\", got %q", logic)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
An empty string is syntactically valid input to `labels.Parse` — it parses as a no-op selector that matches every namespace (equivalent to `labels.Everything()`). Left unchecked, an accidental empty entry in `includedNamespacesByLabel` would silently include all namespaces, and in `excludedNamespacesByLabel` would silently exclude all namespaces. Validation rejects empty (or whitespace-only) selector strings explicitly rather than relying on `labels.Parse` to catch it.
|
||||
|
||||
Validation runs at backup admission time via `prepareBackupRequest`.
|
||||
|
||||
Note: this validates a Kubernetes label selector string via `labels.Parse`, a distinct code path from `wildcard.ValidateNamespaceName` (`pkg/util/wildcard/expand.go`) used by the merged `namespacedFilterPolicies[].namespaces` glob patterns. `wildcard.ValidateNamespaceName` rejects `,`, `(`, `)`, `!`, `{`, `}` because it validates glob syntax; `labels.Parse` requires commas for AND'd requirements (e.g. `team=platform,env=prod`) and supports `!=`, `in`, `notin`, `!key` that glob patterns don't have. Do not reuse `wildcard.ValidateNamespaceName` for these selector strings.
|
||||
|
||||
### Namespace Resolution
|
||||
|
||||
A new helper `resolveNamespacesByLabel` is called in `prepareBackupRequest` (or `kubernetesBackupper`) after the existing namespace filter is constructed. It resolves the included and excluded selector lists **independently** — it must not net one against the other, since the caller (not this helper) is responsible for combining them with `BackupSpec.IncludedNamespaces`/`ExcludedNamespaces` (see [Precedence and Interaction](#precedence-and-interaction)). An earlier draft of this helper subtracted excluded matches from included matches internally and returned only the included list; that silently dropped the excluded set for exclude-only policies (no `includedNamespacesByLabel` configured), where the caller still needs it to subtract from an "all namespaces" baseline. It also takes `logic` ("OR" or "AND", empty treated as "OR") to control how entries within each list are combined — see [Require multiple labels together (AND)](#require-multiple-labels-together-and):
|
||||
|
||||
```go
|
||||
// resolveNamespacesByLabel lists all cluster namespaces and returns two
|
||||
// independently resolved name sets: those matching includedSelectors, and
|
||||
// those matching excludedSelectors, combined per logic ("OR": any selector
|
||||
// matches; "AND": every selector in the list matches; "" defaults to "OR").
|
||||
// It performs no cross-suppression between the two — the caller decides
|
||||
// how to combine them with BackupSpec.IncludedNamespaces/ExcludedNamespaces.
|
||||
func resolveNamespacesByLabel(
|
||||
ctx context.Context,
|
||||
client crclient.Client,
|
||||
includedSelectors []string,
|
||||
excludedSelectors []string,
|
||||
logic string,
|
||||
) (included []string, excluded []string, err error) {
|
||||
nsList := &corev1.NamespaceList{}
|
||||
if err := client.List(ctx, nsList); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "listing namespaces")
|
||||
}
|
||||
|
||||
matchSet := func(selectors []string) sets.String {
|
||||
result := sets.NewString()
|
||||
if len(selectors) == 0 {
|
||||
return result
|
||||
}
|
||||
parsedSelectors := make([]labels.Selector, 0, len(selectors))
|
||||
for _, sel := range selectors {
|
||||
parsed, _ := labels.Parse(sel) // already validated
|
||||
parsedSelectors = append(parsedSelectors, parsed)
|
||||
}
|
||||
for _, ns := range nsList.Items {
|
||||
nsLabels := labels.Set(ns.Labels)
|
||||
if logic == "AND" {
|
||||
allMatch := true
|
||||
for _, parsed := range parsedSelectors {
|
||||
if !parsed.Matches(nsLabels) {
|
||||
allMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allMatch {
|
||||
result.Insert(ns.Name)
|
||||
}
|
||||
} else { // "OR" (default, including "")
|
||||
for _, parsed := range parsedSelectors {
|
||||
if parsed.Matches(nsLabels) {
|
||||
result.Insert(ns.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return matchSet(includedSelectors).List(), matchSet(excludedSelectors).List(), nil
|
||||
}
|
||||
```
|
||||
|
||||
### Precedence and Interaction
|
||||
|
||||
`BackupSpec.IncludedNamespaces` being empty is existing Velero shorthand for "all namespaces." A naive union — `BackupSpec.IncludedNamespaces ∪ resolvedIncludedByLabel` — breaks under that shorthand: an empty `IncludedNamespaces` would expand to "all," and "all" unioned with anything is still "all," silently defeating the entire point of `includedNamespacesByLabel` (the primary use case in this design is a schedule with *no* `includedNamespaces` set, selecting only labeled namespaces).
|
||||
|
||||
The baseline must therefore branch on whether an include-by-label selector is *configured*, not on whether it *matched anything*. These are different signals: a configured selector that currently matches zero namespaces (e.g. nobody has labeled a namespace yet) must still produce an empty baseline, not fall through to "all namespaces" — otherwise a policy author who expects "nothing yet" gets "everything" instead, which is the opposite of fail-safe. The caller must carry this as an explicit flag alongside the resolution call, not infer it from the length of `resolveNamespacesByLabel`'s return value:
|
||||
|
||||
```
|
||||
labelIncludeActive = len(policy.IncludedNamespacesByLabel) > 0 # config presence, NOT len(resolvedIncludedByLabel)
|
||||
resolvedIncludedByLabel, resolvedExcludedByLabel, err = resolveNamespacesByLabel(ctx, client,
|
||||
policy.IncludedNamespacesByLabel, policy.ExcludedNamespacesByLabel, policy.LabelSelectorLogic)
|
||||
|
||||
if labelIncludeActive:
|
||||
baseline = resolvedIncludedByLabel ∪ BackupSpec.IncludedNamespaces # explicit names still additive when both are set
|
||||
# note: if resolvedIncludedByLabel is empty here (selector configured, zero matches),
|
||||
# baseline is empty too (unless BackupSpec.IncludedNamespaces is separately non-empty) — by design.
|
||||
else:
|
||||
baseline = BackupSpec.IncludedNamespaces # empty ⇒ all namespaces, unchanged existing behavior
|
||||
|
||||
effective = baseline
|
||||
− BackupSpec.ExcludedNamespaces
|
||||
− resolvedExcludedByLabel
|
||||
```
|
||||
|
||||
`excludedNamespacesByLabel` is purely subtractive and applies regardless of `labelIncludeActive` — same role as `BackupSpec.ExcludedNamespaces` today, just resolved from labels instead of names. Because `resolveNamespacesByLabel` resolves `resolvedExcludedByLabel` independently of `resolvedIncludedByLabel` (no internal netting), an exclude-only policy (`labelIncludeActive == false`) still gets a correctly populated `resolvedExcludedByLabel` to subtract from the "all namespaces" baseline.
|
||||
|
||||
Worked examples from the [Use Cases](#use-cases) above:
|
||||
- **Dynamic per-schedule targeting**: `includedNamespacesByLabel` set (`labelIncludeActive == true`), `BackupSpec.IncludedNamespaces` empty → baseline is `resolvedIncludedByLabel` only, not all namespaces. If no namespace currently carries the label, baseline is empty and the backup selects nothing — not everything.
|
||||
- **Exclude sensitive namespaces by label**: only `excludedNamespacesByLabel` set (`labelIncludeActive == false`) → baseline falls through to "all namespaces" (today's default), then `resolvedExcludedByLabel` is subtracted.
|
||||
|
||||
`BackupSpec.LabelSelector` and `BackupSpec.OrLabelSelectors` are unaffected — they continue to select individual resources, not namespaces. One trade-off worth calling out explicitly: when `includedNamespacesByLabel` resolves a namespace into the `effective` set above, that namespace's own `Namespace` object is written to the backup even if the namespace doesn't separately match `BackupSpec.LabelSelector`/`OrLabelSelectors` — resource-level label filtering still applies to what's *inside* the namespace, just not to whether the `Namespace` object itself is written. This is not new behavior introduced by this design: it already happens today for any namespace named explicitly in `BackupSpec.IncludedNamespaces`. The `nsTracker` guard in `pkg/backup/item_collector.go` (`namespaceFilter.IncludeEverything() && (singleLabelSelector != nil || len(orLabelSelector) > 0)`) only suppresses a namespace's own object when the namespace filter is at its default — nothing explicitly included (see [#7105](https://github.com/vmware-tanzu/velero/issues/7105), which targeted accidental namespace-object spam from an *unconstrained* namespace sweep, not deliberate inclusion). Once the `effective` set computed above is fed into `backupRequest.NamespaceIncludesExcludes` — the same `namespaceFilter` the guard reads — that filter is no longer at its default, the guard doesn't fire, and the namespace falls through to the pre-existing name-match path (`ShouldInclude`), which has never consulted `LabelSelector`. So namespace-selection-by-label and resource-selection-by-label remain independent axes, matching the existing independent-axes behavior of namespace-selection-by-name — no new guard logic required, no special case.
|
||||
|
||||
**Hard exclusion (`velero.io/exclude-from-backup=true`)**: this is already enforced today in `prepareBackupRequest` (`pkg/controller/backup_controller.go`), which lists namespaces carrying that label and appends them directly into `request.Spec.ExcludedNamespaces` *before* resource-policy processing runs. `resolveNamespacesByLabel` and the effective-set merge above must run after that step (not before), so the `− BackupSpec.ExcludedNamespaces` term already carries the hard-excluded namespaces and the standard set-subtraction removes them — no separate re-check of the label is needed inside `resolveNamespacesByLabel` itself. This is an ordering requirement on where the new resolution call is inserted in `prepareBackupRequest`, not new logic.
|
||||
|
||||
### Observability
|
||||
|
||||
`backup.status.includedNamespaces` does not exist on `BackupStatus` today. Adding it is out of scope for the initial implementation — it would require a new `BackupStatus` field, CRD schema/deepcopy regeneration, and its own compatibility review, none of which this design specifies. Instead, the initial implementation logs during `prepareBackupRequest` so operators can see which namespaces were actually selected: the *count* of resolved namespaces at info level (`len(resolvedIncludedByLabel)`, `len(resolvedExcludedByLabel)`), and the full resolved name list at debug level. A cluster with thousands of namespaces where a selector matches a large fraction of them would otherwise spam Velero's backup logs at info level on every run; count-at-info/list-at-debug avoids that while still making the resolved names available (`--log-level debug` at install, or per-backup via `backup logs`) without changing default log volume. Populating a status field is deferred as a follow-on (see [Open Issues](#open-issues)); if picked up later, it needs its own design pass covering the schema addition and back-compat impact.
|
||||
|
||||
### Limitations
|
||||
|
||||
`includedNamespacesByLabel` and `excludedNamespacesByLabel` do not interact with the old-style global filters (`IncludedResources`/`ExcludedResources`/`IncludeClusterResources` in `BackupSpec`).
|
||||
If a backup references a ResourcePolicy ConfigMap with `includeExcludePolicy` that contains the new fields AND has old-style resource filters in `BackupSpec`, the backup fails validation with a clear error — consistent with the existing behavior of `includeExcludePolicy`.
|
||||
|
||||
**Interaction with `namespacedFilterPolicies`**: If a ResourcePolicy ConfigMap contains both `includedNamespacesByLabel`/`excludedNamespacesByLabel` (from this design) and `namespacedFilterPolicies` (from the [Fine Grained Backup Filters](backup-filter-enhancement/fine-grained-backup-filters-design.md) design), the namespace label selectors determine which namespaces are included in the backup, while `namespacedFilterPolicies` determines which resources within those namespaces are collected. The two mechanisms operate at different levels and are complementary. Concretely: the merged implementation checks `BackupSpec.IncludedNamespaces`/`ExcludedNamespaces` first, then `namespacedFilterPolicies[].namespaces` (glob-matched) against whatever namespace list survives; this design's resolved include/exclude-by-label set feeds into that same effective namespace list, so `namespacedFilterPolicies` transparently applies to namespaces admitted only via a label selector.
|
||||
|
||||
**Not usable for Restore**: `Policies.ValidateForRestore()` (`internal/resourcepolicies/resource_policies.go`) rejects any ResourcePolicy ConfigMap whose `includeExcludePolicy` is non-nil, regardless of which fields are set. A ConfigMap using `includedNamespacesByLabel`/`excludedNamespacesByLabel` for a backup therefore cannot be reused as-is for `RestoreSpec.ResourcePolicy`. This is pre-existing `includeExcludePolicy` behavior, unchanged by this design.
|
||||
|
||||
**Ignored in the global backup volume policies ConfigMap**: `GetGlobalResourcePolicies` applies only the `volumePolicies` section of the cluster-wide global ConfigMap (`--global-volume-policies-configmap` style flow); it logs a warning and ignores `includeExcludePolicy` — and therefore these new fields — if present there. `includedNamespacesByLabel`/`excludedNamespacesByLabel` must be set on a per-backup `BackupSpec.ResourcePolicy` ConfigMap, not the global one.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Modify `BackupSpec.LabelSelector` to select namespaces
|
||||
|
||||
PR [#9223](https://github.com/vmware-tanzu/velero/pull/9223) proposed treating `LabelSelector`-matched namespaces as implicitly included.
|
||||
This was a breaking change and made `LabelSelector` semantics ambiguous (resource filter vs namespace filter).
|
||||
Closed without merge.
|
||||
|
||||
### New CRD field on `BackupSpec` (e.g., `IncludedNamespacesByLabel`)
|
||||
|
||||
Proposed in community meeting but rejected: the community preferred not to proliferate new fields on `BackupSpec` when ResourcePolicy already serves this purpose. This is consistent with the approach taken by the [Fine Grained Backup Filters](backup-filter-enhancement/fine-grained-backup-filters-design.md) design, which also avoids adding new CRD fields.
|
||||
|
||||
### New standalone ConfigMap type
|
||||
|
||||
Adds unnecessary indirection without benefit over extending `includeExcludePolicy` in the existing ResourcePolicy ConfigMap.
|
||||
|
||||
### Match `clusterScopedFilterPolicy`'s `map[string]string` / `[]map[string]string` selector format
|
||||
|
||||
The merged `ResourceFilter.LabelSelector` (`map[string]string`) and `OrLabelSelectors` (`[]map[string]string`) use simple equality maps rather than parsed selector strings. That format was considered here for shape consistency within the same ResourcePolicy ConfigMap. It was rejected: namespace targeting benefits from the full `k8s.io/apimachinery/pkg/labels` selector grammar (`!=`, `in`, `notin`, `!key`) that an equality-only map cannot express, and `includedNamespacesByLabel`/`excludedNamespacesByLabel` are meant to read like `kubectl get ns -l <selector>`, which users already know. The resulting inconsistency in YAML shape across sections of the same ConfigMap (map-based for resource filters, string-based here) is accepted as a deliberate tradeoff, not an oversight.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Label selectors evaluate against live cluster state at backup time.
|
||||
Namespace labels can be changed by anyone with namespace write access, so a user could opt a namespace into or out of a backup schedule by relabeling.
|
||||
Cluster operators should use RBAC to control who can label namespaces if backup inclusion is security-sensitive.
|
||||
|
||||
No new permissions are required by Velero itself — it already has `list` access on namespaces.
|
||||
|
||||
## Compatibility
|
||||
|
||||
This is a backwards-compatible additive change.
|
||||
Existing ResourcePolicy ConfigMaps without the new fields behave identically.
|
||||
Existing backups and schedules are unaffected unless they reference a ConfigMap with the new fields.
|
||||
|
||||
Maintain full backward compatibility — existing backups with no `includedNamespacesByLabel`/`excludedNamespacesByLabel` behave exactly as they do today.
|
||||
|
||||
## User Perspective
|
||||
|
||||
- **For users not using namespace label selector filters**: Zero changes. All existing backups and workflows continue to work identically. The new YAML fields are optional.
|
||||
- **For users adopting namespace label selector filters**: Add `includedNamespacesByLabel` and/or `excludedNamespacesByLabel` to the `includeExcludePolicy` section of the ResourcePolicy ConfigMap, and reference it via `BackupSpec.ResourcePolicy` (or the existing `--resource-policies-configmap` flag). The backup will dynamically resolve which namespaces to include/exclude based on namespace labels at backup time.
|
||||
- **For users already using ResourcePolicy for volume policies or include/exclude policies**: Add the new fields to the existing `includeExcludePolicy` section in the same ConfigMap. All sections coexist.
|
||||
- **Validation errors**: Reported at backup start when the ResourcePolicy ConfigMap contains invalid label selector strings. Consistent with how other validation errors are reported today.
|
||||
|
||||
## Implementation
|
||||
|
||||
1. Add `IncludedNamespacesByLabel` / `ExcludedNamespacesByLabel` / `LabelSelectorLogic` fields to `IncludeExcludePolicy`.
|
||||
2. Extend `Validate()` to parse and validate selector strings using `k8s.io/apimachinery/pkg/labels.Parse`, and to validate `LabelSelectorLogic` is `""`, `"OR"`, or `"AND"`.
|
||||
3. Implement `resolveNamespacesByLabel` helper, taking `LabelSelectorLogic` to combine entries within each list via union (OR) or intersection (AND).
|
||||
4. Call resolution in `prepareBackupRequest` and merge results into the effective namespace filter.
|
||||
5. No new controller-level incompatibility check is needed. `prepareBackupRequest` in `pkg/controller/backup_controller.go` already rejects any non-nil `IncludeExcludePolicy` combined with old-style filters (`collections.UseOldResourceFilters(request.Spec)`) regardless of which `IncludeExcludePolicy` fields are populated. Since the new fields live on the existing `IncludeExcludePolicy` struct, this check covers them automatically.
|
||||
6. Log the count of resolved namespaces at info level, and the full resolved name list at debug level, in `prepareBackupRequest`.
|
||||
7. Add unit tests for selector parsing (including the empty-string rejection), `LabelSelectorLogic` validation and OR/AND resolution behavior, independent resolution of `resolvedIncludedByLabel`/`resolvedExcludedByLabel` (including a configured include selector that matches zero namespaces, and an exclude-only policy), and precedence rules (`labelIncludeActive` baseline branch and hard-exclusion ordering).
|
||||
8. Add E2E test: schedule with no `includedNamespaces`, label selector in resource policy, verify only labeled namespaces are backed up.
|
||||
|
||||
## Open Issues
|
||||
|
||||
- **Status field**: populating `backup.status.includedNamespaces` is deferred as a follow-on; it requires its own design covering the `BackupStatus` schema addition, deepcopy/CRD regeneration, and compatibility impact.
|
||||
Reference in New Issue
Block a user