Files
velero/pkg/util/wildcard/expand.go
T
Joseph Antony VaikathandClaude Sonnet 4.5 43b926a58b Support all glob wildcard characters in namespace validation (#9502)
* Support all glob wildcard characters in namespace validation

Expand namespace validation to allow all valid glob pattern characters
(*, ?, {}, [], ,) by replacing them with valid characters during RFC 1123
validation. The actual glob pattern validation is handled separately by
the wildcard package.

Also add validation to reject unsupported characters (|, (), !) that are
not valid in glob patterns, and update terminology from "regex" to "glob"
for clarity since this implementation uses glob patterns, not regex.

Changes:
- Replace all glob wildcard characters in validateNamespaceName
- Add test coverage for valid glob patterns in includes/excludes
- Add test coverage for unsupported characters
- Reject exclamation mark (!) in wildcard patterns
- Clarify comments and error messages about glob vs regex

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Changelog

Signed-off-by: Joseph <jvaikath@redhat.com>

* Add documentation: glob patterns are now accepted

Signed-off-by: Joseph <jvaikath@redhat.com>

* Error message fix

Signed-off-by: Joseph <jvaikath@redhat.com>

* Remove negation glob char test

Signed-off-by: Joseph <jvaikath@redhat.com>

* Add bracket pattern validation for namespace glob patterns

Extends wildcard validation to support square bracket patterns [] used in glob character classes. Validates bracket syntax including empty brackets, unclosed brackets, and unmatched brackets. Extracts ValidateNamespaceName as a public function to enable reuse in namespace validation logic.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Reduce scope to *, ?, [ and ]

Signed-off-by: Joseph <jvaikath@redhat.com>

* Fix tests

Signed-off-by: Joseph <jvaikath@redhat.com>

* Add namespace glob patterns documentation page

Adds dedicated documentation explaining supported glob patterns
for namespace include/exclude filtering to help users understand
the wildcard syntax.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Fix build-image Dockerfile envtest download

Replace inaccessible go.kubebuilder.io URL with setup-envtest and update envtest version to 1.33.0 to match Kubernetes v0.33.3 dependencies.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* kubebuilder binaries mv

Signed-off-by: Joseph <jvaikath@redhat.com>

* Reject brace patterns and update documentation

Add {, }, and , to unsupported characters list to explicitly reject
brace expansion patterns. Remove { from wildcard detection since these
patterns are not supported in the 1.18 release.

Update all documentation to show supported patterns inline (*, ?, [abc])
with clickable links to the detailed namespace-glob-patterns page.
Simplify YAML comments by removing non-clickable URLs.

Update tests to expect errors when brace patterns are used.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Document brace expansion as unsupported

Add {} and , to the unsupported patterns section to clarify that
brace expansion patterns like {a,b,c} are not supported.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Update tests to expect brace pattern rejection

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

---------

Signed-off-by: Joseph <jvaikath@redhat.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 15:14:06 -05:00

183 lines
5.0 KiB
Go

package wildcard
import (
"errors"
"strings"
"github.com/gobwas/glob"
"k8s.io/apimachinery/pkg/util/sets"
)
func ShouldExpandWildcards(includes []string, excludes []string) bool {
wildcardFound := false
for _, include := range includes {
// Special case: "*" alone means "match all" - don't expand
if include == "*" {
return false
}
if containsWildcardPattern(include) {
wildcardFound = true
}
}
for _, exclude := range excludes {
if containsWildcardPattern(exclude) {
wildcardFound = true
}
}
return wildcardFound
}
// containsWildcardPattern checks if a pattern contains any wildcard symbols
// 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, "*?[")
}
func validateWildcardPatterns(patterns []string) error {
for _, pattern := range patterns {
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 {
bracketDepth := 0
for i := 0; i < len(pattern); i++ {
if pattern[i] == '[' {
bracketStart := i
bracketDepth++
// 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 bracket
i = j
break
}
}
}
// 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 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 ']'")
}
}
return nil
}
func ExpandWildcards(activeNamespaces []string, includes []string, excludes []string) ([]string, []string, error) {
expandedIncludes, err := expandWildcards(includes, activeNamespaces)
if err != nil {
return nil, nil, err
}
expandedExcludes, err := expandWildcards(excludes, activeNamespaces)
if err != nil {
return nil, nil, err
}
return expandedIncludes, expandedExcludes, nil
}
// expands wildcard patterns into a list of namespaces, while normally passing non-wildcard patterns
func expandWildcards(patterns []string, activeNamespaces []string) ([]string, error) {
if len(patterns) == 0 {
return nil, nil
}
// Validate patterns before processing
if err := validateWildcardPatterns(patterns); err != nil {
return nil, err
}
matchedSet := make(map[string]struct{})
for _, pattern := range patterns {
// If the pattern is a non-wildcard pattern, we can just add it to the result
if !containsWildcardPattern(pattern) {
matchedSet[pattern] = struct{}{}
continue
}
// Compile glob pattern
g, err := glob.Compile(pattern)
if err != nil {
return nil, err
}
// Match against all namespaces
for _, ns := range activeNamespaces {
if g.Match(ns) {
matchedSet[ns] = struct{}{}
}
}
}
// Convert set to slice
result := make([]string, 0, len(matchedSet))
for ns := range matchedSet {
result = append(result, ns)
}
return result, nil
}
// GetWildcardResult returns the final list of namespaces after applying wildcard include/exclude logic
func GetWildcardResult(expandedIncludes []string, expandedExcludes []string) []string {
// Set check: set of expandedIncludes - set of expandedExcludes
expandedIncludesSet := sets.New(expandedIncludes...)
expandedExcludesSet := sets.New(expandedExcludes...)
selectedNamespacesSet := expandedIncludesSet.Difference(expandedExcludesSet)
// Convert the set to a slice
selectedNamespaces := make([]string, 0, selectedNamespacesSet.Len())
for ns := range selectedNamespacesSet {
selectedNamespaces = append(selectedNamespaces, ns)
}
return selectedNamespaces
}