mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-08-04 14:26:07 +00:00
Run the E2E test on kind / get-go-version (push) Failing after 1m5s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 2s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 11s
Main CI / Build (push) Failing after 33s
Close stale issues and PRs / stale (push) Successful in 12s
Trivy Nightly Scan / Trivy nightly scan (velero, main) (push) Failing after 1m53s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-aws, main) (push) Failing after 1m8s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-gcp, main) (push) Failing after 1m11s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-microsoft-azure, main) (push) Failing after 1m13s
* Fix wildcard expansion when includes is empty and excludes has wildcards When a Backup CR is applied via kubectl with empty includedNamespaces and a wildcard in excludedNamespaces, ShouldExpandWildcards triggers expansion. The empty includes expands to nil, but wildcardExpanded is set to true, causing ShouldInclude to return false for all namespaces. Populate expanded includes with all active namespaces when the original includes was empty (meaning "include all") so that the wildcardExpanded check does not falsely reject everything. Signed-off-by: Joseph <jvaikath@redhat.com> * Changelog Signed-off-by: Joseph <jvaikath@redhat.com> * Normalize empty includes to * instead of active namespaces list This ensures consistent behavior between CLI and kubectl-apply paths for Namespace CR inclusion when excludes contain wildcards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Joseph <jvaikath@redhat.com> * Move empty includes normalization to backup controller Instead of normalizing empty IncludedNamespaces to ["*"] in the collections layer's ExpandIncludesExcludes, do it earlier in prepareBackupRequest. This ensures the spec is correct before any downstream processing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Joseph <jvaikath@redhat.com> * Update TestProcessBackupCompletions for wildcard normalization Add IncludedNamespaces: []string{"*"} to all expected BackupSpec structs, reflecting the new prepareBackupRequest normalization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Joseph <jvaikath@redhat.com> * Add checks around empty includenamespaces Signed-off-by: Joseph <jvaikath@redhat.com> * gofmt Signed-off-by: Joseph <jvaikath@redhat.com> --------- Signed-off-by: Joseph <jvaikath@redhat.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
188 lines
5.1 KiB
Go
188 lines
5.1 KiB
Go
package wildcard
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/gobwas/glob"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
)
|
|
|
|
func ShouldExpandWildcards(includes []string, excludes []string) bool {
|
|
// Empty includes is equivalent to * (match all) - don't expand
|
|
if len(includes) == 0 {
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|