diff --git a/changelogs/unreleased/9502-Joeavaikath b/changelogs/unreleased/9502-Joeavaikath new file mode 100644 index 000000000..419a5850a --- /dev/null +++ b/changelogs/unreleased/9502-Joeavaikath @@ -0,0 +1 @@ +Support all glob wildcard characters in namespace validation diff --git a/hack/build-image/Dockerfile b/hack/build-image/Dockerfile index 89477d2fb..65378b22e 100644 --- a/hack/build-image/Dockerfile +++ b/hack/build-image/Dockerfile @@ -21,9 +21,11 @@ ENV GO111MODULE=on ENV GOPROXY=${GOPROXY} # kubebuilder test bundle is separated from kubebuilder. Need to setup it for CI test. -RUN curl -sSLo envtest-bins.tar.gz https://go.kubebuilder.io/test-tools/1.22.1/linux/$(go env GOARCH) && \ - mkdir /usr/local/kubebuilder && \ - tar -C /usr/local/kubebuilder --strip-components=1 -zvxf envtest-bins.tar.gz +# Using setup-envtest to download envtest binaries +RUN go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest && \ + mkdir -p /usr/local/kubebuilder/bin && \ + ENVTEST_ASSETS_DIR=$(setup-envtest use 1.33.0 --bin-dir /usr/local/kubebuilder/bin -p path) && \ + cp -r ${ENVTEST_ASSETS_DIR}/* /usr/local/kubebuilder/bin/ RUN wget --quiet https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.2.0/kubebuilder_linux_$(go env GOARCH) && \ mv kubebuilder_linux_$(go env GOARCH) /usr/local/kubebuilder/bin/kubebuilder && \ diff --git a/pkg/util/collections/includes_excludes.go b/pkg/util/collections/includes_excludes.go index b3d18d068..ab63eaa72 100644 --- a/pkg/util/collections/includes_excludes.go +++ b/pkg/util/collections/includes_excludes.go @@ -666,10 +666,22 @@ func validateNamespaceName(ns string) []error { return nil } - // Kubernetes does not allow asterisks in namespaces but Velero uses them as - // wildcards. Replace asterisks with an arbitrary letter to pass Kubernetes - // validation. - tmpNamespace := strings.ReplaceAll(ns, "*", "x") + // Validate the namespace name to ensure it is a valid wildcard pattern + if err := wildcard.ValidateNamespaceName(ns); err != nil { + return []error{err} + } + + // Kubernetes does not allow wildcard characters in namespaces but Velero uses them + // for glob patterns. Replace wildcard characters with valid characters to pass + // Kubernetes validation. + tmpNamespace := ns + + // Replace glob wildcard characters with valid alphanumeric characters + // Note: Validation of wildcard patterns is handled by the wildcard package. + tmpNamespace = strings.ReplaceAll(tmpNamespace, "*", "x") // matches any sequence + tmpNamespace = strings.ReplaceAll(tmpNamespace, "?", "x") // matches single character + tmpNamespace = strings.ReplaceAll(tmpNamespace, "[", "x") // character class start + tmpNamespace = strings.ReplaceAll(tmpNamespace, "]", "x") // character class end if errMsgs := validation.ValidateNamespaceName(tmpNamespace, false); errMsgs != nil { for _, msg := range errMsgs { diff --git a/pkg/util/collections/includes_excludes_test.go b/pkg/util/collections/includes_excludes_test.go index b7fdcd3af..1d700a729 100644 --- a/pkg/util/collections/includes_excludes_test.go +++ b/pkg/util/collections/includes_excludes_test.go @@ -289,6 +289,54 @@ func TestValidateNamespaceIncludesExcludes(t *testing.T) { excludes: []string{"bar"}, wantErr: true, }, + { + name: "glob characters in includes should not error", + includes: []string{"kube-*", "test-?", "ns-[0-9]"}, + excludes: []string{}, + wantErr: false, + }, + { + name: "glob characters in excludes should not error", + includes: []string{"default"}, + excludes: []string{"test-*", "app-?", "ns-[1-5]"}, + wantErr: false, + }, + { + name: "character class in includes should not error", + includes: []string{"ns-[abc]", "test-[0-9]"}, + excludes: []string{}, + wantErr: false, + }, + { + name: "mixed glob patterns should not error", + includes: []string{"kube-*", "test-?"}, + excludes: []string{"*-test", "debug-[0-9]"}, + wantErr: false, + }, + { + name: "pipe character in includes should error", + includes: []string{"namespace|other"}, + excludes: []string{}, + wantErr: true, + }, + { + name: "parentheses in includes should error", + includes: []string{"namespace(prod)", "test-(dev)"}, + excludes: []string{}, + wantErr: true, + }, + { + name: "exclamation mark in includes should error", + includes: []string{"!namespace", "test!"}, + excludes: []string{}, + wantErr: true, + }, + { + name: "unsupported characters in excludes should error", + includes: []string{"default"}, + excludes: []string{"test|prod", "app(staging)"}, + wantErr: true, + }, } for _, tc := range tests { @@ -1082,16 +1130,6 @@ func TestExpandIncludesExcludes(t *testing.T) { expectedWildcardExpanded: true, expectError: false, }, - { - name: "brace wildcard pattern", - includes: []string{"app-{prod,dev}"}, - excludes: []string{}, - activeNamespaces: []string{"app-prod", "app-dev", "app-test", "default"}, - expectedIncludes: []string{"app-prod", "app-dev"}, - expectedExcludes: []string{}, - expectedWildcardExpanded: true, - expectError: false, - }, { name: "empty activeNamespaces with wildcards", includes: []string{"kube-*"}, @@ -1233,13 +1271,6 @@ func TestResolveNamespaceList(t *testing.T) { expectedNamespaces: []string{"kube-system", "kube-public"}, preExpandWildcards: true, }, - { - name: "complex wildcard pattern", - includes: []string{"app-{prod,dev}", "kube-*"}, - excludes: []string{"*-test"}, - activeNamespaces: []string{"app-prod", "app-dev", "app-test", "kube-system", "kube-test", "default"}, - expectedNamespaces: []string{"app-prod", "app-dev", "kube-system"}, - }, { name: "question mark wildcard pattern", includes: []string{"ns-?"}, diff --git a/pkg/util/wildcard/expand.go b/pkg/util/wildcard/expand.go index 632e05aa1..8767c8ed3 100644 --- a/pkg/util/wildcard/expand.go +++ b/pkg/util/wildcard/expand.go @@ -31,70 +31,77 @@ func ShouldExpandWildcards(includes []string, excludes []string) bool { } // containsWildcardPattern checks if a pattern contains any wildcard symbols -// Supported patterns: *, ?, [abc], {a,b,c} +// Supported patterns: *, ?, [abc] // Note: . and + are treated as literal characters (not wildcards) // Note: ** and consecutive asterisks are NOT supported (will cause validation error) func containsWildcardPattern(pattern string) bool { - return strings.ContainsAny(pattern, "*?[{") + return strings.ContainsAny(pattern, "*?[") } func validateWildcardPatterns(patterns []string) error { for _, pattern := range patterns { - // Check for invalid regex-only patterns that we don't support - if strings.ContainsAny(pattern, "|()") { - return errors.New("wildcard pattern contains unsupported regex symbols: |, (, )") - } - - // Check for consecutive asterisks (2 or more) - if strings.Contains(pattern, "**") { - return errors.New("wildcard pattern contains consecutive asterisks (only single * allowed)") - } - - // Check for malformed brace patterns - if err := validateBracePatterns(pattern); err != nil { + if err := ValidateNamespaceName(pattern); err != nil { return err } } return nil } +func ValidateNamespaceName(pattern string) error { + // Check for invalid characters that are not supported in glob patterns + if strings.ContainsAny(pattern, "|()!{},") { + return errors.New("wildcard pattern contains unsupported characters: |, (, ), !, {, }, ,") + } + + // Check for consecutive asterisks (2 or more) + if strings.Contains(pattern, "**") { + return errors.New("wildcard pattern contains consecutive asterisks (only single * allowed)") + } + + // Check for malformed brace patterns + if err := validateBracePatterns(pattern); err != nil { + return err + } + + return nil +} + // validateBracePatterns checks for malformed brace patterns like unclosed braces or empty braces +// Also validates bracket patterns [] for character classes func validateBracePatterns(pattern string) error { - depth := 0 + bracketDepth := 0 for i := 0; i < len(pattern); i++ { - if pattern[i] == '{' { - braceStart := i - depth++ + if pattern[i] == '[' { + bracketStart := i + bracketDepth++ - // Scan ahead to find the matching closing brace and validate content - for j := i + 1; j < len(pattern) && depth > 0; j++ { - if pattern[j] == '{' { - depth++ - } else if pattern[j] == '}' { - depth-- - if depth == 0 { - // Found matching closing brace - validate content - content := pattern[braceStart+1 : j] - if strings.Trim(content, ", \t") == "" { - return errors.New("wildcard pattern contains empty brace pattern '{}'") + // Scan ahead to find the matching closing bracket and validate content + for j := i + 1; j < len(pattern) && bracketDepth > 0; j++ { + if pattern[j] == ']' { + bracketDepth-- + if bracketDepth == 0 { + // Found matching closing bracket - validate content + content := pattern[bracketStart+1 : j] + if content == "" { + return errors.New("wildcard pattern contains empty bracket pattern '[]'") } - // Skip to the closing brace + // Skip to the closing bracket i = j break } } } - // If we exited the loop without finding a match (depth > 0), brace is unclosed - if depth > 0 { - return errors.New("wildcard pattern contains unclosed brace '{'") + // If we exited the loop without finding a match (bracketDepth > 0), bracket is unclosed + if bracketDepth > 0 { + return errors.New("wildcard pattern contains unclosed bracket '['") } - // i is now positioned at the closing brace; the outer loop will increment it - } else if pattern[i] == '}' { - // Found a closing brace without a matching opening brace - return errors.New("wildcard pattern contains unmatched closing brace '}'") + // i is now positioned at the closing bracket; the outer loop will increment it + } else if pattern[i] == ']' { + // Found a closing bracket without a matching opening bracket + return errors.New("wildcard pattern contains unmatched closing bracket ']'") } } diff --git a/pkg/util/wildcard/expand_test.go b/pkg/util/wildcard/expand_test.go index f6c7ed434..317020648 100644 --- a/pkg/util/wildcard/expand_test.go +++ b/pkg/util/wildcard/expand_test.go @@ -90,7 +90,7 @@ func TestShouldExpandWildcards(t *testing.T) { name: "brace alternatives wildcard", includes: []string{"ns{prod,staging}"}, excludes: []string{}, - expected: true, // brace alternatives are considered wildcard + expected: false, // brace alternatives are not supported }, { name: "dot is literal - not wildcard", @@ -237,9 +237,9 @@ func TestExpandWildcards(t *testing.T) { activeNamespaces: []string{"app-prod", "app-staging", "app-dev", "db-prod"}, includes: []string{"app-{prod,staging}"}, excludes: []string{}, - expectedIncludes: []string{"app-prod", "app-staging"}, // {prod,staging} matches either + expectedIncludes: nil, expectedExcludes: nil, - expectError: false, + expectError: true, }, { name: "literal dot and plus patterns", @@ -259,33 +259,6 @@ func TestExpandWildcards(t *testing.T) { expectedExcludes: nil, expectError: true, // |, (, ) are not supported }, - { - name: "unclosed brace patterns should error", - activeNamespaces: []string{"app-prod"}, - includes: []string{"app-{prod,staging"}, - excludes: []string{}, - expectedIncludes: nil, - expectedExcludes: nil, - expectError: true, // unclosed brace - }, - { - name: "empty brace patterns should error", - activeNamespaces: []string{"app-prod"}, - includes: []string{"app-{}"}, - excludes: []string{}, - expectedIncludes: nil, - expectedExcludes: nil, - expectError: true, // empty braces - }, - { - name: "unmatched closing brace should error", - activeNamespaces: []string{"app-prod"}, - includes: []string{"app-prod}"}, - excludes: []string{}, - expectedIncludes: nil, - expectedExcludes: nil, - expectError: true, // unmatched closing brace - }, } for _, tt := range tests { @@ -354,13 +327,6 @@ func TestExpandWildcardsPrivate(t *testing.T) { expected: []string{}, // returns empty slice, not nil expectError: false, }, - { - name: "brace patterns work correctly", - patterns: []string{"app-{prod,staging}"}, - activeNamespaces: []string{"app-prod", "app-staging", "app-dev", "app-{prod,staging}"}, - expected: []string{"app-prod", "app-staging"}, // brace patterns do expand - expectError: false, - }, { name: "duplicate matches from multiple patterns", patterns: []string{"app-*", "*-prod"}, @@ -389,20 +355,6 @@ func TestExpandWildcardsPrivate(t *testing.T) { expected: []string{"nsa", "nsb", "nsc"}, // [a-c] matches a to c expectError: false, }, - { - name: "negated character class", - patterns: []string{"ns[!abc]"}, - activeNamespaces: []string{"nsa", "nsb", "nsc", "nsd", "ns1"}, - expected: []string{"nsd", "ns1"}, // [!abc] matches anything except a, b, c - expectError: false, - }, - { - name: "brace alternatives", - patterns: []string{"app-{prod,test}"}, - activeNamespaces: []string{"app-prod", "app-test", "app-staging", "db-prod"}, - expected: []string{"app-prod", "app-test"}, // {prod,test} matches either - expectError: false, - }, { name: "double asterisk should error", patterns: []string{"**"}, @@ -410,13 +362,6 @@ func TestExpandWildcardsPrivate(t *testing.T) { expected: nil, expectError: true, // ** is not allowed }, - { - name: "literal dot and plus", - patterns: []string{"app.prod", "service+"}, - activeNamespaces: []string{"app.prod", "appXprod", "service+", "service"}, - expected: []string{"app.prod", "service+"}, // . and + are literal - expectError: false, - }, { name: "unsupported regex symbols should error", patterns: []string{"ns(1|2)"}, @@ -468,153 +413,101 @@ func TestValidateBracePatterns(t *testing.T) { expectError bool errorMsg string }{ - // Valid patterns + // Valid square bracket patterns { - name: "valid single brace pattern", - pattern: "app-{prod,staging}", + name: "valid square bracket pattern", + pattern: "ns[abc]", expectError: false, }, { - name: "valid brace with single option", - pattern: "app-{prod}", + name: "valid square bracket pattern with range", + pattern: "ns[a-z]", expectError: false, }, { - name: "valid brace with three options", - pattern: "app-{prod,staging,dev}", + name: "valid square bracket pattern with numbers", + pattern: "ns[0-9]", expectError: false, }, { - name: "valid pattern with text before and after brace", - pattern: "prefix-{a,b}-suffix", + name: "valid square bracket pattern with mixed", + pattern: "ns[a-z0-9]", expectError: false, }, { - name: "valid pattern with no braces", - pattern: "app-prod", + name: "valid square bracket pattern with single character", + pattern: "ns[a]", expectError: false, }, { - name: "valid pattern with asterisk", - pattern: "app-*", + name: "valid square bracket pattern with text before and after", + pattern: "prefix-[abc]-suffix", expectError: false, }, + // Unclosed opening brackets { - name: "valid brace with spaces around content", - pattern: "app-{ prod , staging }", - expectError: false, + name: "unclosed opening bracket at end", + pattern: "ns[abc", + expectError: true, + errorMsg: "unclosed bracket", }, { - name: "valid brace with numbers", - pattern: "ns-{1,2,3}", - expectError: false, + name: "unclosed opening bracket at start", + pattern: "[abc", + expectError: true, + errorMsg: "unclosed bracket", }, { - name: "valid brace with hyphens in options", - pattern: "{app-prod,db-staging}", - expectError: false, + name: "unclosed opening bracket in middle", + pattern: "ns[abc-test", + expectError: true, + errorMsg: "unclosed bracket", }, - // Unclosed opening braces + // Unmatched closing brackets { - name: "unclosed opening brace at end", - pattern: "app-{prod,staging", + name: "unmatched closing bracket at end", + pattern: "ns-abc]", expectError: true, - errorMsg: "unclosed brace", + errorMsg: "unmatched closing bracket", }, { - name: "unclosed opening brace at start", - pattern: "{prod,staging", + name: "unmatched closing bracket at start", + pattern: "]ns-abc", expectError: true, - errorMsg: "unclosed brace", + errorMsg: "unmatched closing bracket", }, { - name: "unclosed opening brace in middle", - pattern: "app-{prod-test", + name: "unmatched closing bracket in middle", + pattern: "ns-]abc", expectError: true, - errorMsg: "unclosed brace", + errorMsg: "unmatched closing bracket", }, { - name: "multiple unclosed braces", - pattern: "app-{prod-{staging", + name: "extra closing bracket after valid pair", + pattern: "ns[abc]]", expectError: true, - errorMsg: "unclosed brace", + errorMsg: "unmatched closing bracket", }, - // Unmatched closing braces + // Empty bracket patterns { - name: "unmatched closing brace at end", - pattern: "app-prod}", + name: "completely empty brackets", + pattern: "ns[]", expectError: true, - errorMsg: "unmatched closing brace", + errorMsg: "empty bracket pattern", }, { - name: "unmatched closing brace at start", - pattern: "}app-prod", + name: "empty brackets at start", + pattern: "[]ns", expectError: true, - errorMsg: "unmatched closing brace", + errorMsg: "empty bracket pattern", }, { - name: "unmatched closing brace in middle", - pattern: "app-}prod", + name: "empty brackets standalone", + pattern: "[]", expectError: true, - errorMsg: "unmatched closing brace", - }, - { - name: "extra closing brace after valid pair", - pattern: "app-{prod,staging}}", - expectError: true, - errorMsg: "unmatched closing brace", - }, - - // Empty brace patterns - { - name: "completely empty braces", - pattern: "app-{}", - expectError: true, - errorMsg: "empty brace pattern", - }, - { - name: "braces with only spaces", - pattern: "app-{ }", - expectError: true, - errorMsg: "empty brace pattern", - }, - { - name: "braces with only comma", - pattern: "app-{,}", - expectError: true, - errorMsg: "empty brace pattern", - }, - { - name: "braces with only commas", - pattern: "app-{,,,}", - expectError: true, - errorMsg: "empty brace pattern", - }, - { - name: "braces with commas and spaces", - pattern: "app-{ , , }", - expectError: true, - errorMsg: "empty brace pattern", - }, - { - name: "braces with tabs and commas", - pattern: "app-{\t,\t}", - expectError: true, - errorMsg: "empty brace pattern", - }, - { - name: "empty braces at start", - pattern: "{}app-prod", - expectError: true, - errorMsg: "empty brace pattern", - }, - { - name: "empty braces standalone", - pattern: "{}", - expectError: true, - errorMsg: "empty brace pattern", + errorMsg: "empty bracket pattern", }, // Edge cases @@ -623,58 +516,6 @@ func TestValidateBracePatterns(t *testing.T) { pattern: "", expectError: false, }, - { - name: "pattern with only opening brace", - pattern: "{", - expectError: true, - errorMsg: "unclosed brace", - }, - { - name: "pattern with only closing brace", - pattern: "}", - expectError: true, - errorMsg: "unmatched closing brace", - }, - { - name: "valid brace with special characters inside", - pattern: "app-{prod-1,staging_2,dev.3}", - expectError: false, - }, - { - name: "brace with asterisk inside option", - pattern: "app-{prod*,staging}", - expectError: false, - }, - { - name: "multiple valid brace patterns", - pattern: "{app,db}-{prod,staging}", - expectError: false, - }, - { - name: "brace with single character", - pattern: "app-{a}", - expectError: false, - }, - { - name: "brace with trailing comma but has content", - pattern: "app-{prod,staging,}", - expectError: false, // Has content, so it's valid - }, - { - name: "brace with leading comma but has content", - pattern: "app-{,prod,staging}", - expectError: false, // Has content, so it's valid - }, - { - name: "brace with leading comma but has content", - pattern: "app-{{,prod,staging}", - expectError: true, // unclosed brace - }, - { - name: "brace with leading comma but has content", - pattern: "app-{,prod,staging}}", - expectError: true, // unmatched closing brace - }, } for _, tt := range tests { @@ -723,20 +564,6 @@ func TestExpandWildcardsEdgeCases(t *testing.T) { assert.ElementsMatch(t, []string{"ns-1", "ns_2", "ns.3", "ns@4"}, result) }) - t.Run("complex glob combinations", func(t *testing.T) { - activeNamespaces := []string{"app1-prod", "app2-prod", "app1-test", "db-prod", "service"} - result, err := expandWildcards([]string{"app?-{prod,test}"}, activeNamespaces) - require.NoError(t, err) - assert.ElementsMatch(t, []string{"app1-prod", "app2-prod", "app1-test"}, result) - }) - - t.Run("escaped characters", func(t *testing.T) { - activeNamespaces := []string{"app*", "app-prod", "app?test", "app-test"} - result, err := expandWildcards([]string{"app\\*"}, activeNamespaces) - require.NoError(t, err) - assert.ElementsMatch(t, []string{"app*"}, result) - }) - t.Run("mixed literal and wildcard patterns", func(t *testing.T) { activeNamespaces := []string{"app.prod", "app-prod", "app_prod", "test.ns"} result, err := expandWildcards([]string{"app.prod", "app?prod"}, activeNamespaces) @@ -777,12 +604,8 @@ func TestExpandWildcardsEdgeCases(t *testing.T) { shouldError bool }{ {"unclosed bracket", "ns[abc", true}, - {"unclosed brace", "app-{prod,staging", true}, - {"nested unclosed", "ns[a{bc", true}, {"valid bracket", "ns[abc]", false}, - {"valid brace", "app-{prod,staging}", false}, {"empty bracket", "ns[]", true}, // empty brackets are invalid - {"empty brace", "app-{}", true}, // empty braces are invalid } for _, tt := range tests { diff --git a/site/content/docs/main/api-types/backup.md b/site/content/docs/main/api-types/backup.md index 1768f0934..3bad516e3 100644 --- a/site/content/docs/main/api-types/backup.md +++ b/site/content/docs/main/api-types/backup.md @@ -16,6 +16,8 @@ Backup belongs to the API group version `velero.io/v1`. Here is a sample `Backup` object with each of the fields documented: +**Note:** Namespace includes/excludes support glob patterns (`*`, `?`, `[abc]`). See [Namespace Glob Patterns](../namespace-glob-patterns) for more details. + ```yaml # Standard Kubernetes API Version declaration. Required. apiVersion: velero.io/v1 @@ -42,11 +44,12 @@ spec: resourcePolicy: kind: configmap name: resource-policy-configmap - # Array of namespaces to include in the backup. If unspecified, all namespaces are included. - # Optional. + # Array of namespaces to include in the backup. Accepts glob patterns (*, ?, [abc]). + # Note: '*' alone is reserved for empty fields, which means all namespaces. + # If unspecified, all namespaces are included. Optional. includedNamespaces: - '*' - # Array of namespaces to exclude from the backup. Optional. + # Array of namespaces to exclude from the backup. Accepts glob patterns (*, ?, [abc]). Optional. excludedNamespaces: - some-namespace # Array of resources to include in the backup. Resources may be shortcuts (for example 'po' for 'pods') diff --git a/site/content/docs/main/api-types/restore.md b/site/content/docs/main/api-types/restore.md index ec3e19511..1c80a0ee8 100644 --- a/site/content/docs/main/api-types/restore.md +++ b/site/content/docs/main/api-types/restore.md @@ -16,6 +16,8 @@ Restore belongs to the API group version `velero.io/v1`. Here is a sample `Restore` object with each of the fields documented: +**Note:** Namespace includes/excludes support glob patterns (`*`, `?`, `[abc]`). See [Namespace Glob Patterns](../namespace-glob-patterns) for more details. + ```yaml # Standard Kubernetes API Version declaration. Required. apiVersion: velero.io/v1 @@ -45,11 +47,11 @@ spec: writeSparseFiles: true # ParallelFilesDownload is the concurrency number setting for restore parallelFilesDownload: 10 - # Array of namespaces to include in the restore. If unspecified, all namespaces are included. - # Optional. + # Array of namespaces to include in the restore. Accepts glob patterns (*, ?, [abc]). + # If unspecified, all namespaces are included. Optional. includedNamespaces: - '*' - # Array of namespaces to exclude from the restore. Optional. + # Array of namespaces to exclude from the restore. Accepts glob patterns (*, ?, [abc]). Optional. excludedNamespaces: - some-namespace # Array of resources to include in the restore. Resources may be shortcuts (for example 'po' for 'pods') diff --git a/site/content/docs/main/namespace-glob-patterns.md b/site/content/docs/main/namespace-glob-patterns.md new file mode 100644 index 000000000..4695124ea --- /dev/null +++ b/site/content/docs/main/namespace-glob-patterns.md @@ -0,0 +1,71 @@ +--- +title: "Namespace Glob Patterns" +layout: docs +--- + +When using `--include-namespaces` and `--exclude-namespaces` flags with backup and restore commands, you can use glob patterns to match multiple namespaces. + +## Supported Patterns + +Velero supports the following glob pattern characters: + +- `*` - Matches any sequence of characters + ```bash + velero backup create my-backup --include-namespaces "app-*" + # Matches: app-prod, app-staging, app-dev, etc. + ``` + +- `?` - Matches exactly one character + ```bash + velero backup create my-backup --include-namespaces "ns?" + # Matches: ns1, ns2, nsa, but NOT ns10 + ``` + +- `[abc]` - Matches any single character in the brackets + ```bash + velero backup create my-backup --include-namespaces "ns[123]" + # Matches: ns1, ns2, ns3 + ``` + +- `[a-z]` - Matches any single character in the range + ```bash + velero backup create my-backup --include-namespaces "ns[a-c]" + # Matches: nsa, nsb, nsc + ``` + +## Unsupported Patterns + +The following patterns are **not supported** and will cause validation errors: + +- `**` - Consecutive asterisks +- `|` - Alternation (regex operator) +- `()` - Grouping (regex operators) +- `!` - Negation +- `{}` - Brace expansion +- `,` - Comma (used in brace expansion) + +## Special Cases + +- `*` alone means "all namespaces" and is not expanded +- Empty brackets `[]` are invalid +- Unmatched or unclosed brackets will cause validation errors + +## Examples + +Combine patterns with include and exclude flags: + +```bash +# Backup all production namespaces except test +velero backup create prod-backup \ + --include-namespaces "*-prod" \ + --exclude-namespaces "test-*" + +# Backup specific numbered namespaces +velero backup create numbered-backup \ + --include-namespaces "app-[0-9]" + +# Restore namespaces matching multiple patterns +velero restore create my-restore \ + --from-backup my-backup \ + --include-namespaces "frontend-*,backend-*" +``` diff --git a/site/content/docs/main/resource-filtering.md b/site/content/docs/main/resource-filtering.md index a9e65d157..cbfdb2816 100644 --- a/site/content/docs/main/resource-filtering.md +++ b/site/content/docs/main/resource-filtering.md @@ -17,7 +17,11 @@ Wildcard takes precedence when both a wildcard and specific resource are include ### --include-namespaces -Namespaces to include. Default is `*`, all namespaces. +Namespaces to include. Accepts glob patterns (`*`, `?`, `[abc]`). Default is `*`, all namespaces. + +See [Namespace Glob Patterns](namespace-glob-patterns) for more details on supported patterns. + +Note: `*` alone is reserved for empty fields, which means all namespaces. * Backup a namespace and it's objects. @@ -158,7 +162,9 @@ Wildcard excludes are ignored. ### --exclude-namespaces -Namespaces to exclude. +Namespaces to exclude. Accepts glob patterns (`*`, `?`, `[abc]`). + +See [Namespace Glob Patterns](namespace-glob-patterns.md) for more details on supported patterns. * Exclude kube-system from the cluster backup. diff --git a/site/content/docs/v1.18/api-types/backup.md b/site/content/docs/v1.18/api-types/backup.md index 1768f0934..634264ee1 100644 --- a/site/content/docs/v1.18/api-types/backup.md +++ b/site/content/docs/v1.18/api-types/backup.md @@ -16,6 +16,8 @@ Backup belongs to the API group version `velero.io/v1`. Here is a sample `Backup` object with each of the fields documented: +**Note:** Namespace includes/excludes support glob patterns (`*`, `?`, `[abc]`). See [Namespace Glob Patterns](../namespace-glob-patterns) for more details. + ```yaml # Standard Kubernetes API Version declaration. Required. apiVersion: velero.io/v1 @@ -42,11 +44,11 @@ spec: resourcePolicy: kind: configmap name: resource-policy-configmap - # Array of namespaces to include in the backup. If unspecified, all namespaces are included. - # Optional. + # Array of namespaces to include in the backup. Accepts glob patterns (*, ?, [abc]). + # If unspecified, all namespaces are included. Optional. includedNamespaces: - '*' - # Array of namespaces to exclude from the backup. Optional. + # Array of namespaces to exclude from the backup. Accepts glob patterns (*, ?, [abc]). Optional. excludedNamespaces: - some-namespace # Array of resources to include in the backup. Resources may be shortcuts (for example 'po' for 'pods') diff --git a/site/content/docs/v1.18/api-types/restore.md b/site/content/docs/v1.18/api-types/restore.md index ec3e19511..1c80a0ee8 100644 --- a/site/content/docs/v1.18/api-types/restore.md +++ b/site/content/docs/v1.18/api-types/restore.md @@ -16,6 +16,8 @@ Restore belongs to the API group version `velero.io/v1`. Here is a sample `Restore` object with each of the fields documented: +**Note:** Namespace includes/excludes support glob patterns (`*`, `?`, `[abc]`). See [Namespace Glob Patterns](../namespace-glob-patterns) for more details. + ```yaml # Standard Kubernetes API Version declaration. Required. apiVersion: velero.io/v1 @@ -45,11 +47,11 @@ spec: writeSparseFiles: true # ParallelFilesDownload is the concurrency number setting for restore parallelFilesDownload: 10 - # Array of namespaces to include in the restore. If unspecified, all namespaces are included. - # Optional. + # Array of namespaces to include in the restore. Accepts glob patterns (*, ?, [abc]). + # If unspecified, all namespaces are included. Optional. includedNamespaces: - '*' - # Array of namespaces to exclude from the restore. Optional. + # Array of namespaces to exclude from the restore. Accepts glob patterns (*, ?, [abc]). Optional. excludedNamespaces: - some-namespace # Array of resources to include in the restore. Resources may be shortcuts (for example 'po' for 'pods') diff --git a/site/content/docs/v1.18/namespace-glob-patterns.md b/site/content/docs/v1.18/namespace-glob-patterns.md new file mode 100644 index 000000000..6a2f9f0e3 --- /dev/null +++ b/site/content/docs/v1.18/namespace-glob-patterns.md @@ -0,0 +1,71 @@ +--- +title: "Namespace Glob Patterns" +layout: docs +--- + +When using `--include-namespaces` and `--exclude-namespaces` flags with backup and restore commands, you can use glob patterns to match multiple namespaces. + +## Supported Patterns + +Velero supports the following glob pattern characters: + +- `*` - Matches any sequence of characters + ```bash + velero backup create my-backup --include-namespaces "app-*" + # Matches: app-prod, app-staging, app-dev, etc. + ``` + +- `?` - Matches exactly one character + ```bash + velero backup create my-backup --include-namespaces "ns?" + # Matches: ns1, ns2, nsa, but NOT ns10 + ``` + +- `[abc]` - Matches any single character in the brackets + ```bash + velero backup create my-backup --include-namespaces "ns[123]" + # Matches: ns1, ns2, ns3 + ``` + +- `[a-z]` - Matches any single character in the range + ```bash + velero backup create my-backup --include-namespaces "ns[a-c]" + # Matches: nsa, nsb, nsc + ``` + +## Unsupported Patterns + +The following patterns are **not supported** and will cause validation errors: + +- `**` - Consecutive asterisks +- `|` - Alternation (regex operator) +- `()` - Grouping (regex operators) +- `!` - Negation +- `{}` - Brace expansion +- `,` - Comma (used in brace expansion) + +## Special Cases + +- `*` alone means "all namespaces" and is not expanded +- Empty brackets `[]` are invalid +- Unmatched or unclosed brackets will cause validation errors + +## Examples + +Combine patterns with include and exclude flags: + +```bash +# Backup all production namespaces except test +velero backup create prod-backup \ + --include-namespaces "*-prod" \ + --exclude-namespaces "test-*" + +# Backup specific numbered namespaces +velero backup create numbered-backup \ + --include-namespaces "app-[0-9]" + +# Restore namespaces matching multiple patterns +velero restore create my-restore \ + --from-backup my-backup \ + --include-namespaces "frontend-*,backend-*" +``` diff --git a/site/content/docs/v1.18/resource-filtering.md b/site/content/docs/v1.18/resource-filtering.md index a9e65d157..69b2cb5e1 100644 --- a/site/content/docs/v1.18/resource-filtering.md +++ b/site/content/docs/v1.18/resource-filtering.md @@ -17,7 +17,11 @@ Wildcard takes precedence when both a wildcard and specific resource are include ### --include-namespaces -Namespaces to include. Default is `*`, all namespaces. +Namespaces to include. Accepts glob patterns (`*`, `?`, `[abc]`). Default is `*`, all namespaces. + +See [Namespace Glob Patterns](namespace-glob-patterns) for more details on supported patterns. + +Note: `*` alone is reserved for empty fields, which means all namespaces. * Backup a namespace and it's objects. @@ -158,7 +162,9 @@ Wildcard excludes are ignored. ### --exclude-namespaces -Namespaces to exclude. +Namespaces to exclude. Accepts glob patterns (`*`, `?`, `[abc]`). + +See [Namespace Glob Patterns](namespace-glob-patterns) for more details on supported patterns. * Exclude kube-system from the cluster backup. diff --git a/site/data/docs/main-toc.yml b/site/data/docs/main-toc.yml index c732a9174..dacec0651 100644 --- a/site/data/docs/main-toc.yml +++ b/site/data/docs/main-toc.yml @@ -33,6 +33,8 @@ toc: url: /enable-api-group-versions-feature - page: Resource filtering url: /resource-filtering + - page: Namespace glob patterns + url: /namespace-glob-patterns - page: Backup reference url: /backup-reference - page: Backup hooks diff --git a/site/data/docs/v1-18-toc.yml b/site/data/docs/v1-18-toc.yml index c732a9174..dacec0651 100644 --- a/site/data/docs/v1-18-toc.yml +++ b/site/data/docs/v1-18-toc.yml @@ -33,6 +33,8 @@ toc: url: /enable-api-group-versions-feature - page: Resource filtering url: /resource-filtering + - page: Namespace glob patterns + url: /namespace-glob-patterns - page: Backup reference url: /backup-reference - page: Backup hooks