mirror of
https://github.com/vmware-tanzu/velero.git
synced 2025-12-23 06:15:21 +00:00
Wildcard ns implement (#9255)
Some checks failed
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 3s
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 25s
Close stale issues and PRs / stale (push) Successful in 14s
Trivy Nightly Scan / Trivy nightly scan (velero, main) (push) Failing after 1m36s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-aws, main) (push) Failing after 1m2s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-gcp, main) (push) Failing after 1m19s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-microsoft-azure, main) (push) Failing after 1m2s
Some checks failed
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 3s
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 25s
Close stale issues and PRs / stale (push) Successful in 14s
Trivy Nightly Scan / Trivy nightly scan (velero, main) (push) Failing after 1m36s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-aws, main) (push) Failing after 1m2s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-gcp, main) (push) Failing after 1m19s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-microsoft-azure, main) (push) Failing after 1m2s
* Add wildcard status fields Signed-off-by: Joseph <jvaikath@redhat.com> * Implement wildcard namespace expansion in item collector - Introduced methods to get active namespaces and expand wildcard includes/excludes in the item collector. - Updated getNamespacesToList to handle wildcard patterns and return expanded lists. - Added utility functions for setting includes and excludes in the IncludesExcludes struct. - Created a new package for wildcard handling, including functions to determine when to expand wildcards and to perform the expansion. This enhances the backup process by allowing more flexible namespace selection based on wildcard patterns. Signed-off-by: Joseph <jvaikath@redhat.com> * Enhance wildcard expansion logic and logging in item collector - Improved logging to include original includes and excludes when expanding wildcards. - Updated the ShouldExpandWildcards function to check for wildcard patterns in excludes. - Added comments for clarity in the expandWildcards function regarding pattern handling. These changes enhance the clarity and functionality of the wildcard expansion process in the backup system. Signed-off-by: Joseph <jvaikath@redhat.com> * Add wildcard namespace fields to Backup CRD and update deepcopy methods - Introduced `wildcardIncludedNamespaces` and `wildcardExcludedNamespaces` fields to the Backup CRD to support wildcard patterns for namespace inclusion and exclusion. - Updated deepcopy methods to handle the new fields, ensuring proper copying of data during object manipulation. These changes enhance the flexibility of namespace selection in backup operations, aligning with recent improvements in wildcard handling. Signed-off-by: Joseph <jvaikath@redhat.com> * Refactor Backup CRD to rename wildcard namespace fields - Updated `BackupStatus` struct to rename `WildcardIncludedNamespaces` to `WildcardExpandedIncludedNamespaces` and `WildcardExcludedNamespaces` to `WildcardExpandedExcludedNamespaces` for clarity. - Adjusted associated comments to reflect the new naming and ensure consistency in documentation. - Modified deepcopy methods to accommodate the renamed fields, ensuring proper data handling during object manipulation. These changes enhance the clarity and maintainability of the Backup CRD, aligning with recent improvements in wildcard handling. Signed-off-by: Joseph <jvaikath@redhat.com> * Fix Signed-off-by: Joseph <jvaikath@redhat.com> * Refactor where wildcard expansion happens Signed-off-by: Joseph <jvaikath@redhat.com> * Refactor Backup CRD and related components for expanded namespace handling - Updated `BackupStatus` struct to rename fields for clarity: `WildcardExpandedIncludedNamespaces` and `WildcardExpandedExcludedNamespaces` are now `ExpandedIncludedNamespaces` and `ExpandedExcludedNamespaces`, respectively. - Adjusted associated comments and deepcopy methods to reflect the new naming conventions. - Removed the `getActiveNamespaces` function from the item collector, streamlining the namespace handling process. - Enhanced logging during wildcard expansion to provide clearer insights into the process. These changes improve the clarity and maintainability of the Backup CRD and enhance the functionality of namespace selection in backup operations. Signed-off-by: Joseph <jvaikath@redhat.com> * Refactor wildcard expansion logic in item collector and enhance testing - Moved the wildcard expansion logic into a dedicated method, `expandNamespaceWildcards`, improving code organization and readability. - Updated logging to provide detailed insights during the wildcard expansion process. - Introduced comprehensive unit tests for wildcard handling, covering various scenarios and edge cases. - Enhanced the `ShouldExpandWildcards` function to better identify wildcard patterns and validate inputs. These changes improve the maintainability and robustness of the wildcard handling in the backup system. Signed-off-by: Joseph <jvaikath@redhat.com> * Enhance Restore CRD with expanded namespace fields and update logic - Added `ExpandedIncludedNamespaces` and `ExpandedExcludedNamespaces` fields to the `RestoreStatus` struct to support expanded wildcard namespace handling. - Updated the `DeepCopyInto` method to ensure proper copying of the new fields. - Implemented logic in the restore process to expand wildcard patterns for included and excluded namespaces, improving flexibility in namespace selection during restores. - Enhanced logging to provide insights into the expanded namespaces. These changes improve the functionality and maintainability of the restore process, aligning with recent enhancements in wildcard handling. Signed-off-by: Joseph <jvaikath@redhat.com> * Refactor Backup and Restore CRDs to enhance wildcard namespace handling - Renamed fields in `BackupStatus` and `RestoreStatus` from `ExpandedIncludedNamespaces` and `ExpandedExcludedNamespaces` to `IncludeWildcardMatches` and `ExcludeWildcardMatches` for clarity. - Introduced a new field `WildcardResult` to record the final namespaces after applying wildcard logic. - Updated the `DeepCopyInto` methods to accommodate the new field names and ensure proper data handling. - Enhanced comments to reflect the changes and improve documentation clarity. These updates improve the maintainability and clarity of the CRDs, aligning with recent enhancements in wildcard handling. Signed-off-by: Joseph <jvaikath@redhat.com> * Enhance wildcard namespace handling in Backup and Restore processes - Updated `BackupRequest` and `Restore` status structures to include a new field `WildcardResult`, which captures the final list of namespaces after applying wildcard logic. - Renamed existing fields to `IncludeWildcardMatches` and `ExcludeWildcardMatches` for improved clarity. - Enhanced logging to provide detailed insights into the expanded namespaces and final results during backup and restore operations. - Introduced a new utility function `GetWildcardResult` to streamline the selection of namespaces based on include/exclude criteria. These changes improve the clarity and functionality of namespace selection in both backup and restore processes, aligning with recent enhancements in wildcard handling. Signed-off-by: Joseph <jvaikath@redhat.com> * Refactor namespace wildcard expansion logic in restore process - Moved the wildcard expansion logic into a dedicated method, `expandNamespaceWildcards`, improving code organization and readability. - Enhanced error handling and logging to provide detailed insights into the expanded namespaces during the restore operation. - Updated the restore context with expanded namespace patterns and final results, ensuring clarity in the restore status. These changes improve the maintainability and clarity of the restore process, aligning with recent enhancements in wildcard handling. Signed-off-by: Joseph <jvaikath@redhat.com> * Add checks for "*" in exclude Signed-off-by: Joseph <jvaikath@redhat.com> * Rebase Signed-off-by: Joseph <jvaikath@redhat.com> * Create NamespaceIncludesExcludes to get full NS listing for backup w/ Signed-off-by: Scott Seago <sseago@redhat.com> Signed-off-by: Joseph <jvaikath@redhat.com> * Add new NamespaceIncludesExcludes struct Signed-off-by: Joseph <jvaikath@redhat.com> * Move namespace expansion logic Signed-off-by: Joseph <jvaikath@redhat.com> * Update backup status with expansion Signed-off-by: Joseph <jvaikath@redhat.com> * Wildcard status update Signed-off-by: Joseph <jvaikath@redhat.com> * Skip ns check if wildcard expansion Signed-off-by: Joseph <jvaikath@redhat.com> * Move wildcard expansion to getResourceItems Signed-off-by: Joseph <jvaikath@redhat.com> * lint Signed-off-by: Joseph <jvaikath@redhat.com> * Changelog Signed-off-by: Joseph <jvaikath@redhat.com> * linting issues Signed-off-by: Joseph <jvaikath@redhat.com> * Remove wildcard restore to check if tests pass Signed-off-by: Joseph <jvaikath@redhat.com> * Fix namespace mapping test bug from lint fix The previous commit (0a4aabcf4) attempted to fix linting issues by using strings.Builder, but incorrectly wrote commas to a separate builder and concatenated them at the end instead of between namespace mappings. This caused the namespace mapping string to be malformed: Before: ns-1:ns-1-mapped,ns-2:ns-2-mapped Bug: ns-1:ns-1-mappedns-2:ns-2-mapped,, The malformed string was parsed as a single mapping with an invalid namespace name containing a colon, causing Kubernetes to reject it: "ns-1-mappedns-2:ns-2-mapped" is invalid Fix by properly using strings.Builder to construct the mapping string with commas between entries, addressing both the linting concern and the functional bug. Fixes the MultiNamespacesMappingResticTest and MultiNamespacesMappingSnapshotTest failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> Signed-off-by: Joseph <jvaikath@redhat.com> * Fix wildcard namespace expansion edge cases This commit fixes two bugs in the wildcard namespace expansion feature: 1. Empty wildcard results: When a wildcard pattern (e.g., "invalid*") matched no namespaces, the backup would incorrectly back up ALL namespaces instead of backing up nothing. This was because the empty includes list was indistinguishable from "no filter specified". Fix: Added wildcardExpanded flag to NamespaceIncludesExcludes to track when wildcard expansion has occurred. When true and the includes list is empty, ShouldInclude now correctly returns false. 2. Premature namespace filtering: An earlier attempt to fix bug #1 filtered namespaces too early in collectNamespaces, breaking LabelSelector tests where namespaces should be included based on resources within them matching the label selector. Fix: Removed the premature filtering and rely on the existing filterNamespaces call at the end of getAllItems, which correctly handles both wildcard expansion and label selector scenarios. The fixes ensure: - Wildcard patterns matching nothing result in empty backups - Label selectors still work correctly (namespace included if any resource in it matches the selector) - State is preserved across multiple ResolveNamespaceList calls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> Signed-off-by: Joseph <jvaikath@redhat.com> * Run wildcard expansion during backup processing Signed-off-by: Joseph <jvaikath@redhat.com> * Lint fix Signed-off-by: Joseph <jvaikath@redhat.com> * Improve coverage Signed-off-by: Joseph <jvaikath@redhat.com> * gofmt fix Signed-off-by: Joseph <jvaikath@redhat.com> * Add wildcard details to describe backup status Signed-off-by: Joseph <jvaikath@redhat.com> * Revert "Remove wildcard restore to check if tests pass" This reverts commit 4e22c2af855b71447762cb0a9fab7e7049f38a5f. Signed-off-by: Joseph <jvaikath@redhat.com> * Add restore describe for wildcard namespaces Revert restore wildcard removal Signed-off-by: Joseph <jvaikath@redhat.com> * Add coverage Signed-off-by: Joseph <jvaikath@redhat.com> * Lint Signed-off-by: Joseph <jvaikath@redhat.com> * Remove unintentional changes Signed-off-by: Joseph <jvaikath@redhat.com> * Remove wildcard status fields and mentionsRemove usage of wildcard fields for backup and restore status. Signed-off-by: Joseph <jvaikath@redhat.com> * Remove status update changelog line Signed-off-by: Joseph <jvaikath@redhat.com> * Rename getNamespaceIncludesExcludes Signed-off-by: Scott Seago <sseago@redhat.com> Signed-off-by: Scott Seago <sseago@redhat.com> * Rewrite brace pattern validation Signed-off-by: Joseph <jvaikath@redhat.com> * Different var for internal loop Signed-off-by: Joseph <jvaikath@redhat.com> --------- Signed-off-by: Joseph <jvaikath@redhat.com> Signed-off-by: Scott Seago <sseago@redhat.com> Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com> Co-authored-by: Scott Seago <sseago@redhat.com> Co-authored-by: Tiger Kaovilai <tkaovila@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
d96434c8c9
commit
975f647323
10
changelogs/unreleased/9255-Joeavaikath
Normal file
10
changelogs/unreleased/9255-Joeavaikath
Normal file
@@ -0,0 +1,10 @@
|
||||
Implement wildcard namespace pattern expansion for backup namespace includes/excludes.
|
||||
|
||||
This change adds support for wildcard patterns (*, ?, [abc], {a,b,c}) in namespace includes and excludes during backup operations.
|
||||
When wildcard patterns are detected, they are expanded against the list of active namespaces in the cluster before the backup proceeds.
|
||||
|
||||
Key features:
|
||||
- Wildcard patterns in namespace includes/excludes are automatically detected and expanded
|
||||
- Pattern validation ensures unsupported patterns (regex, consecutive asterisks) are rejected
|
||||
- Empty wildcard results (e.g., "invalid*" matching no namespaces) correctly result in empty backups
|
||||
- Exact namespace names and "*" continue to work as before (no expansion needed)
|
||||
@@ -168,10 +168,39 @@ func NewKubernetesBackupper(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getNamespaceIncludesExcludes returns an IncludesExcludes list containing which namespaces to
|
||||
// include and exclude from the backup.
|
||||
func getNamespaceIncludesExcludes(backup *velerov1api.Backup) *collections.IncludesExcludes {
|
||||
return collections.NewIncludesExcludes().Includes(backup.Spec.IncludedNamespaces...).Excludes(backup.Spec.ExcludedNamespaces...)
|
||||
// getNamespaceIncludesExcludesAndArgoCDNamespaces returns an IncludesExcludes list containing which namespaces to
|
||||
// include and exclude from the backup and a list of namespaces managed by ArgoCD.
|
||||
func getNamespaceIncludesExcludesAndArgoCDNamespaces(backup *velerov1api.Backup, kbClient kbclient.Client) (*collections.NamespaceIncludesExcludes, []string, error) {
|
||||
nsList := corev1api.NamespaceList{}
|
||||
activeNamespaces := []string{}
|
||||
nsManagedByArgoCD := []string{}
|
||||
if err := kbClient.List(context.Background(), &nsList); err != nil {
|
||||
return nil, nsManagedByArgoCD, err
|
||||
}
|
||||
for _, ns := range nsList.Items {
|
||||
activeNamespaces = append(activeNamespaces, ns.Name)
|
||||
}
|
||||
|
||||
// Set ActiveNamespaces first, then set includes/excludes
|
||||
includesExcludes := collections.NewNamespaceIncludesExcludes().
|
||||
ActiveNamespaces(activeNamespaces).
|
||||
Includes(backup.Spec.IncludedNamespaces...).
|
||||
Excludes(backup.Spec.ExcludedNamespaces...)
|
||||
|
||||
// Expand wildcards if needed
|
||||
if err := includesExcludes.ExpandIncludesExcludes(); err != nil {
|
||||
return nil, []string{}, err
|
||||
}
|
||||
|
||||
// Check for ArgoCD managed namespaces in the namespaces that will be included
|
||||
for _, ns := range nsList.Items {
|
||||
nsLabels := ns.GetLabels()
|
||||
if len(nsLabels[ArgoCDManagedByNamespaceLabel]) > 0 && includesExcludes.ShouldInclude(ns.Name) {
|
||||
nsManagedByArgoCD = append(nsManagedByArgoCD, ns.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return includesExcludes, nsManagedByArgoCD, nil
|
||||
}
|
||||
|
||||
func getResourceHooks(hookSpecs []velerov1api.BackupResourceHookSpec, discoveryHelper discovery.Helper) ([]hook.ResourceHook, error) {
|
||||
@@ -245,8 +274,35 @@ func (kb *kubernetesBackupper) BackupWithResolvers(
|
||||
if err := kb.writeBackupVersion(tw); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
var err error
|
||||
var nsManagedByArgoCD []string
|
||||
backupRequest.NamespaceIncludesExcludes, nsManagedByArgoCD, err = getNamespaceIncludesExcludesAndArgoCDNamespaces(backupRequest.Backup, kb.kbClient)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("error getting namespace includes/excludes")
|
||||
return err
|
||||
}
|
||||
|
||||
if backupRequest.NamespaceIncludesExcludes.IsWildcardExpanded() {
|
||||
expandedIncludes := backupRequest.NamespaceIncludesExcludes.GetIncludes()
|
||||
expandedExcludes := backupRequest.NamespaceIncludesExcludes.GetExcludes()
|
||||
|
||||
// Get the final namespace list after wildcard expansion
|
||||
wildcardResult, err := backupRequest.NamespaceIncludesExcludes.ResolveNamespaceList()
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("error resolving namespace list")
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"expandedIncludes": expandedIncludes,
|
||||
"expandedExcludes": expandedExcludes,
|
||||
"wildcardResult": wildcardResult,
|
||||
"includedCount": len(expandedIncludes),
|
||||
"excludedCount": len(expandedExcludes),
|
||||
"resultCount": len(wildcardResult),
|
||||
}).Info("Successfully expanded wildcard patterns")
|
||||
}
|
||||
|
||||
backupRequest.NamespaceIncludesExcludes = getNamespaceIncludesExcludes(backupRequest.Backup)
|
||||
log.Infof("Including namespaces: %s", backupRequest.NamespaceIncludesExcludes.IncludesString())
|
||||
log.Infof("Excluding namespaces: %s", backupRequest.NamespaceIncludesExcludes.ExcludesString())
|
||||
|
||||
@@ -254,13 +310,9 @@ func (kb *kubernetesBackupper) BackupWithResolvers(
|
||||
// We will check for the existence of a ArgoCD label in the includedNamespaces and add a warning
|
||||
// so that users are at least aware about the existence of argoCD managed ns in their backup
|
||||
// Related Issue: https://github.com/vmware-tanzu/velero/issues/7905
|
||||
if len(backupRequest.Spec.IncludedNamespaces) > 0 {
|
||||
nsManagedByArgoCD := getNamespacesManagedByArgoCD(kb.kbClient, backupRequest.Spec.IncludedNamespaces, log)
|
||||
|
||||
if len(nsManagedByArgoCD) > 0 {
|
||||
log.Warnf("backup operation may encounter complications and potentially produce undesirable results due to the inclusion of namespaces %v managed by ArgoCD in the backup.", nsManagedByArgoCD)
|
||||
}
|
||||
}
|
||||
|
||||
if collections.UseOldResourceFilters(backupRequest.Spec) {
|
||||
backupRequest.ResourceIncludesExcludes = collections.GetGlobalResourceIncludesExcludes(kb.discoveryHelper, log,
|
||||
@@ -284,7 +336,6 @@ func (kb *kubernetesBackupper) BackupWithResolvers(
|
||||
|
||||
log.Infof("Backing up all volumes using pod volume backup: %t", boolptr.IsSetToTrue(backupRequest.Backup.Spec.DefaultVolumesToFsBackup))
|
||||
|
||||
var err error
|
||||
backupRequest.ResourceHooks, err = getResourceHooks(backupRequest.Spec.Hooks.Resources, kb.discoveryHelper)
|
||||
if err != nil {
|
||||
log.WithError(errors.WithStack(err)).Debugf("Error from getResourceHooks")
|
||||
@@ -1256,26 +1307,3 @@ func putVolumeInfos(
|
||||
|
||||
return backupStore.PutBackupVolumeInfos(backupName, backupVolumeInfoBuf)
|
||||
}
|
||||
|
||||
func getNamespacesManagedByArgoCD(kbClient kbclient.Client, includedNamespaces []string, log logrus.FieldLogger) []string {
|
||||
var nsManagedByArgoCD []string
|
||||
|
||||
for _, nsName := range includedNamespaces {
|
||||
ns := corev1api.Namespace{}
|
||||
if err := kbClient.Get(context.Background(), kbclient.ObjectKey{Name: nsName}, &ns); err != nil {
|
||||
// check for only those ns that exist and are included in backup
|
||||
// here we ignore cases like "" or "*" specified under includedNamespaces
|
||||
if apierrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
log.WithError(err).Errorf("error getting namespace %s", nsName)
|
||||
continue
|
||||
}
|
||||
|
||||
nsLabels := ns.GetLabels()
|
||||
if len(nsLabels[ArgoCDManagedByNamespaceLabel]) > 0 {
|
||||
nsManagedByArgoCD = append(nsManagedByArgoCD, nsName)
|
||||
}
|
||||
}
|
||||
return nsManagedByArgoCD
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ type itemCollector struct {
|
||||
type nsTracker struct {
|
||||
singleLabelSelector labels.Selector
|
||||
orLabelSelector []labels.Selector
|
||||
namespaceFilter *collections.IncludesExcludes
|
||||
namespaceFilter *collections.NamespaceIncludesExcludes
|
||||
logger logrus.FieldLogger
|
||||
|
||||
namespaceMap map[string]bool
|
||||
@@ -103,7 +103,7 @@ func (nt *nsTracker) init(
|
||||
unstructuredNSs []unstructured.Unstructured,
|
||||
singleLabelSelector labels.Selector,
|
||||
orLabelSelector []labels.Selector,
|
||||
namespaceFilter *collections.IncludesExcludes,
|
||||
namespaceFilter *collections.NamespaceIncludesExcludes,
|
||||
logger logrus.FieldLogger,
|
||||
) {
|
||||
if nt.namespaceMap == nil {
|
||||
@@ -635,7 +635,7 @@ func coreGroupResourcePriority(resource string) int {
|
||||
// getNamespacesToList examines ie and resolves the includes and excludes to a full list of
|
||||
// namespaces to list. If ie is nil or it includes *, the result is just "" (list across all
|
||||
// namespaces). Otherwise, the result is a list of every included namespace minus all excluded ones.
|
||||
func getNamespacesToList(ie *collections.IncludesExcludes) []string {
|
||||
func getNamespacesToList(ie *collections.NamespaceIncludesExcludes) []string {
|
||||
if ie == nil {
|
||||
return []string{""}
|
||||
}
|
||||
@@ -753,22 +753,29 @@ func (r *itemCollector) collectNamespaces(
|
||||
}
|
||||
|
||||
unstructuredList, err := resourceClient.List(metav1.ListOptions{})
|
||||
|
||||
activeNamespacesHashSet := make(map[string]bool)
|
||||
for _, namespace := range unstructuredList.Items {
|
||||
activeNamespacesHashSet[namespace.GetName()] = true
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithError(errors.WithStack(err)).Error("error list namespaces")
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, includedNSName := range r.backupRequest.Backup.Spec.IncludedNamespaces {
|
||||
// Change to look at the struct includes/excludes
|
||||
// In case wildcards are expanded, we need to look at the struct includes/excludes
|
||||
for _, includedNSName := range r.backupRequest.NamespaceIncludesExcludes.GetIncludes() {
|
||||
nsExists := false
|
||||
// Skip checking the namespace existing when it's "*".
|
||||
if includedNSName == "*" {
|
||||
continue
|
||||
}
|
||||
for _, unstructuredNS := range unstructuredList.Items {
|
||||
if unstructuredNS.GetName() == includedNSName {
|
||||
|
||||
if _, ok := activeNamespacesHashSet[includedNSName]; ok {
|
||||
nsExists = true
|
||||
}
|
||||
}
|
||||
|
||||
if !nsExists {
|
||||
log.Errorf("fail to get the namespace %s specified in backup.Spec.IncludedNamespaces", includedNSName)
|
||||
@@ -809,17 +816,18 @@ func (r *itemCollector) collectNamespaces(
|
||||
var items []*kubernetesResource
|
||||
|
||||
for index := range unstructuredList.Items {
|
||||
nsName := unstructuredList.Items[index].GetName()
|
||||
|
||||
path, err := r.writeToFile(&unstructuredList.Items[index])
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Error writing item %s to file",
|
||||
unstructuredList.Items[index].GetName())
|
||||
log.WithError(err).Errorf("Error writing item %s to file", nsName)
|
||||
continue
|
||||
}
|
||||
|
||||
items = append(items, &kubernetesResource{
|
||||
groupResource: gr,
|
||||
preferredGVR: preferredGVR,
|
||||
name: unstructuredList.Items[index].GetName(),
|
||||
name: nsName,
|
||||
path: path,
|
||||
kind: resource.Kind,
|
||||
})
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestFilterNamespaces(t *testing.T) {
|
||||
func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ie *collections.IncludesExcludes
|
||||
ie *collections.NamespaceIncludesExcludes
|
||||
namespaces []*corev1api.Namespace
|
||||
backup *velerov1api.Backup
|
||||
expectedTrackedNS []string
|
||||
@@ -162,7 +162,7 @@ func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
{
|
||||
name: "ns filter by namespace IE filter",
|
||||
backup: builder.ForBackup("velero", "backup").Result(),
|
||||
ie: collections.NewIncludesExcludes().Includes("ns1"),
|
||||
ie: collections.NewNamespaceIncludesExcludes().Includes("ns1"),
|
||||
namespaces: []*corev1api.Namespace{
|
||||
builder.ForNamespace("ns1").Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
|
||||
@@ -174,7 +174,7 @@ func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
backup: builder.ForBackup("velero", "backup").LabelSelector(&metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"name": "ns1"},
|
||||
}).Result(),
|
||||
ie: collections.NewIncludesExcludes().Includes("*"),
|
||||
ie: collections.NewNamespaceIncludesExcludes().Includes("*"),
|
||||
namespaces: []*corev1api.Namespace{
|
||||
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
|
||||
@@ -186,7 +186,7 @@ func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
backup: builder.ForBackup("velero", "backup").OrLabelSelector([]*metav1.LabelSelector{
|
||||
{MatchLabels: map[string]string{"name": "ns1"}},
|
||||
}).Result(),
|
||||
ie: collections.NewIncludesExcludes().Includes("*"),
|
||||
ie: collections.NewNamespaceIncludesExcludes().Includes("*"),
|
||||
namespaces: []*corev1api.Namespace{
|
||||
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
|
||||
@@ -198,7 +198,7 @@ func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
backup: builder.ForBackup("velero", "backup").LabelSelector(&metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"name": "ns1"},
|
||||
}).Result(),
|
||||
ie: collections.NewIncludesExcludes().Excludes("ns1"),
|
||||
ie: collections.NewNamespaceIncludesExcludes().Excludes("ns1"),
|
||||
namespaces: []*corev1api.Namespace{
|
||||
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
|
||||
@@ -210,7 +210,7 @@ func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
backup: builder.ForBackup("velero", "backup").OrLabelSelector([]*metav1.LabelSelector{
|
||||
{MatchLabels: map[string]string{"name": "ns1"}},
|
||||
}).Result(),
|
||||
ie: collections.NewIncludesExcludes().Excludes("ns1", "ns2"),
|
||||
ie: collections.NewNamespaceIncludesExcludes().Excludes("ns1", "ns2"),
|
||||
namespaces: []*corev1api.Namespace{
|
||||
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
|
||||
@@ -221,7 +221,7 @@ func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
{
|
||||
name: "No ns filters",
|
||||
backup: builder.ForBackup("velero", "backup").Result(),
|
||||
ie: collections.NewIncludesExcludes().Includes("*"),
|
||||
ie: collections.NewNamespaceIncludesExcludes().Includes("*"),
|
||||
namespaces: []*corev1api.Namespace{
|
||||
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
|
||||
@@ -231,7 +231,7 @@ func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
{
|
||||
name: "ns specified by the IncludeNamespaces cannot be found",
|
||||
backup: builder.ForBackup("velero", "backup").IncludedNamespaces("ns1", "invalid", "*").Result(),
|
||||
ie: collections.NewIncludesExcludes().Includes("ns1", "invalid", "*"),
|
||||
ie: collections.NewNamespaceIncludesExcludes().Includes("ns1", "invalid", "*"),
|
||||
namespaces: []*corev1api.Namespace{
|
||||
builder.ForNamespace("ns1").ObjectMeta(builder.WithLabels("name", "ns1")).Phase(corev1api.NamespaceActive).Result(),
|
||||
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
|
||||
@@ -242,7 +242,7 @@ func TestItemCollectorBackupNamespaces(t *testing.T) {
|
||||
{
|
||||
name: "terminating ns should not tracked",
|
||||
backup: builder.ForBackup("velero", "backup").Result(),
|
||||
ie: collections.NewIncludesExcludes().Includes("ns1", "ns2"),
|
||||
ie: collections.NewNamespaceIncludesExcludes().Includes("ns1", "ns2"),
|
||||
namespaces: []*corev1api.Namespace{
|
||||
builder.ForNamespace("ns1").Phase(corev1api.NamespaceTerminating).Result(),
|
||||
builder.ForNamespace("ns2").Phase(corev1api.NamespaceActive).Result(),
|
||||
|
||||
@@ -57,7 +57,7 @@ type Request struct {
|
||||
*velerov1api.Backup
|
||||
StorageLocation *velerov1api.BackupStorageLocation
|
||||
SnapshotLocations []*velerov1api.VolumeSnapshotLocation
|
||||
NamespaceIncludesExcludes *collections.IncludesExcludes
|
||||
NamespaceIncludesExcludes *collections.NamespaceIncludesExcludes
|
||||
ResourceIncludesExcludes collections.IncludesExcludesInterface
|
||||
ResourceHooks []hook.ResourceHook
|
||||
ResolvedActions []framework.BackupItemResolvedActionV2
|
||||
|
||||
@@ -315,8 +315,14 @@ func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) {
|
||||
}
|
||||
|
||||
// DescribeBackupStatus describes a backup status in human-readable format.
|
||||
func DescribeBackupStatus(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup, details bool,
|
||||
insecureSkipTLSVerify bool, caCertPath string, podVolumeBackups []velerov1api.PodVolumeBackup) {
|
||||
func DescribeBackupStatus(ctx context.Context,
|
||||
kbClient kbclient.Client,
|
||||
d *Describer,
|
||||
backup *velerov1api.Backup,
|
||||
details bool,
|
||||
insecureSkipTLSVerify bool,
|
||||
caCertPath string,
|
||||
podVolumeBackups []velerov1api.PodVolumeBackup) {
|
||||
status := backup.Status
|
||||
|
||||
// Status.Version has been deprecated, use Status.FormatVersion
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -77,6 +78,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/kube"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/results"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/wildcard"
|
||||
)
|
||||
|
||||
const ObjectStatusRestoreAnnotationKey = "velero.io/restore-status"
|
||||
@@ -474,6 +476,12 @@ func (ctx *restoreContext) execute() (results.Result, results.Result) {
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
// Expand wildcard patterns in namespace includes/excludes if needed
|
||||
if err := ctx.expandNamespaceWildcards(backupResources); err != nil {
|
||||
errs.AddVeleroError(err)
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
// TODO: Remove outer feature flag check to make this feature a default in Velero.
|
||||
if features.IsEnabled(velerov1api.APIGroupVersionsFeatureFlag) {
|
||||
if ctx.backup.Status.FormatVersion >= "1.1.0" {
|
||||
@@ -2378,6 +2386,59 @@ func (ctx *restoreContext) getSelectedRestoreableItems(resource string, original
|
||||
return restorable, warnings, errs
|
||||
}
|
||||
|
||||
// extractNamespacesFromBackup extracts all available namespaces from backup resources
|
||||
func extractNamespacesFromBackup(backupResources map[string]*archive.ResourceItems) []string {
|
||||
namespaceSet := make(map[string]struct{})
|
||||
for _, resource := range backupResources {
|
||||
for namespace := range resource.ItemsByNamespace {
|
||||
if namespace != "" { // Skip cluster-scoped resources (empty namespace)
|
||||
namespaceSet[namespace] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespaces := make([]string, 0, len(namespaceSet))
|
||||
for ns := range namespaceSet {
|
||||
namespaces = append(namespaces, ns)
|
||||
}
|
||||
return namespaces
|
||||
}
|
||||
|
||||
// expandNamespaceWildcards expands wildcard patterns in namespace includes/excludes
|
||||
// and updates the restore context with the expanded patterns and status
|
||||
func (ctx *restoreContext) expandNamespaceWildcards(backupResources map[string]*archive.ResourceItems) error {
|
||||
if !wildcard.ShouldExpandWildcards(ctx.restore.Spec.IncludedNamespaces, ctx.restore.Spec.ExcludedNamespaces) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If `*` is mentioned in restore excludes, something is wrong
|
||||
if slices.Contains(ctx.restore.Spec.ExcludedNamespaces, "*") {
|
||||
return errors.New("wildcard '*' is not allowed in restore excludes")
|
||||
}
|
||||
|
||||
availableNamespaces := extractNamespacesFromBackup(backupResources)
|
||||
expandedIncludes, expandedExcludes, err := wildcard.ExpandWildcards(
|
||||
availableNamespaces,
|
||||
ctx.restore.Spec.IncludedNamespaces,
|
||||
ctx.restore.Spec.ExcludedNamespaces,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error expanding wildcard patterns in namespace includes/excludes")
|
||||
}
|
||||
|
||||
// Update namespace includes/excludes with expanded patterns
|
||||
ctx.namespaceIncludesExcludes = collections.NewIncludesExcludes().
|
||||
Includes(expandedIncludes...).
|
||||
Excludes(expandedExcludes...)
|
||||
|
||||
selectedNamespaces := wildcard.GetWildcardResult(expandedIncludes, expandedExcludes)
|
||||
|
||||
ctx.log.Infof("Expanded namespace wildcards - includes: %v, excludes: %v, final: %v",
|
||||
expandedIncludes, expandedExcludes, selectedNamespaces)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeRestoreLabels removes the restore name and the
|
||||
// restored backup's name.
|
||||
func removeRestoreLabels(obj metav1.Object) {
|
||||
|
||||
241
pkg/restore/restore_wildcard_test.go
Normal file
241
pkg/restore/restore_wildcard_test.go
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
Copyright the Velero contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package restore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/archive"
|
||||
)
|
||||
|
||||
func TestExpandNamespaceWildcards(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
includeNamespaces []string
|
||||
excludeNamespaces []string
|
||||
backupResources map[string]*archive.ResourceItems
|
||||
expectedIncludeMatches []string
|
||||
expectedExcludeMatches []string
|
||||
expectedWildcardResult []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "No wildcards - should not expand",
|
||||
includeNamespaces: []string{"ns1", "ns2"},
|
||||
excludeNamespaces: []string{"ns3"},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"ns1": {}, "ns2": {}, "ns3": {}}},
|
||||
},
|
||||
expectedIncludeMatches: nil,
|
||||
expectedExcludeMatches: nil,
|
||||
expectedWildcardResult: nil,
|
||||
},
|
||||
{
|
||||
name: "Simple wildcard include pattern",
|
||||
includeNamespaces: []string{"test*"},
|
||||
excludeNamespaces: []string{},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"test1": {}, "test2": {}, "prod1": {}}},
|
||||
},
|
||||
expectedIncludeMatches: []string{"test1", "test2"},
|
||||
expectedExcludeMatches: []string{},
|
||||
expectedWildcardResult: []string{"test1", "test2"},
|
||||
},
|
||||
{
|
||||
name: "Multiple wildcard patterns",
|
||||
includeNamespaces: []string{"test*", "dev*"},
|
||||
excludeNamespaces: []string{},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"test1": {}, "test2": {}, "dev1": {}, "prod1": {}}},
|
||||
},
|
||||
expectedIncludeMatches: []string{"dev1", "test1", "test2"},
|
||||
expectedExcludeMatches: []string{},
|
||||
expectedWildcardResult: []string{"dev1", "test1", "test2"},
|
||||
},
|
||||
{
|
||||
name: "Wildcard include with wildcard exclude",
|
||||
includeNamespaces: []string{"test*"},
|
||||
excludeNamespaces: []string{"*-temp"},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"test1": {}, "test2-temp": {}, "test3": {}}},
|
||||
},
|
||||
expectedIncludeMatches: []string{"test1", "test2-temp", "test3"},
|
||||
expectedExcludeMatches: []string{"test2-temp"},
|
||||
expectedWildcardResult: []string{"test1", "test3"},
|
||||
},
|
||||
{
|
||||
name: "Wildcard include with literal exclude",
|
||||
includeNamespaces: []string{"app-*"},
|
||||
excludeNamespaces: []string{"app-test"},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"app-prod": {}, "app-test": {}, "app-dev": {}}},
|
||||
},
|
||||
expectedIncludeMatches: []string{"app-dev", "app-prod", "app-test"},
|
||||
expectedExcludeMatches: []string{"app-test"},
|
||||
expectedWildcardResult: []string{"app-dev", "app-prod"},
|
||||
},
|
||||
{
|
||||
name: "Error: wildcard * in excludes",
|
||||
includeNamespaces: []string{"test*"},
|
||||
excludeNamespaces: []string{"*"},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"test1": {}}},
|
||||
},
|
||||
expectedError: "wildcard '*' is not allowed in restore excludes",
|
||||
},
|
||||
{
|
||||
name: "Empty backup - no matches",
|
||||
includeNamespaces: []string{"test*"},
|
||||
excludeNamespaces: []string{},
|
||||
backupResources: map[string]*archive.ResourceItems{},
|
||||
expectedIncludeMatches: []string{},
|
||||
expectedExcludeMatches: []string{},
|
||||
expectedWildcardResult: []string{},
|
||||
},
|
||||
{
|
||||
name: "Wildcard with no matches",
|
||||
includeNamespaces: []string{"nonexistent*"},
|
||||
excludeNamespaces: []string{},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"test1": {}, "test2": {}}},
|
||||
},
|
||||
expectedIncludeMatches: []string{},
|
||||
expectedExcludeMatches: []string{},
|
||||
expectedWildcardResult: []string{},
|
||||
},
|
||||
{
|
||||
name: "Complex pattern with prefix and suffix",
|
||||
includeNamespaces: []string{"app-*-prod"},
|
||||
excludeNamespaces: []string{},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"app-frontend-prod": {}, "app-backend-prod": {}, "app-frontend-dev": {}}},
|
||||
},
|
||||
expectedIncludeMatches: []string{"app-backend-prod", "app-frontend-prod"},
|
||||
expectedExcludeMatches: []string{},
|
||||
expectedWildcardResult: []string{"app-backend-prod", "app-frontend-prod"},
|
||||
},
|
||||
{
|
||||
name: "Backup with cluster resources",
|
||||
includeNamespaces: []string{"test*"},
|
||||
excludeNamespaces: []string{},
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"test1": {}, "test2": {}}},
|
||||
"persistentvolumes": {ItemsByNamespace: map[string][]string{"": {}}}, // cluster-scoped
|
||||
"pods.v1": {ItemsByNamespace: map[string][]string{"test1": {"pod1"}}},
|
||||
},
|
||||
expectedIncludeMatches: []string{"test1", "test2"},
|
||||
expectedExcludeMatches: []string{},
|
||||
expectedWildcardResult: []string{"test1", "test2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
restore := &velerov1api.Restore{
|
||||
Spec: velerov1api.RestoreSpec{
|
||||
IncludedNamespaces: tc.includeNamespaces,
|
||||
ExcludedNamespaces: tc.excludeNamespaces,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := &restoreContext{
|
||||
restore: restore,
|
||||
log: logrus.StandardLogger(),
|
||||
}
|
||||
|
||||
err := ctx.expandNamespaceWildcards(tc.backupResources)
|
||||
|
||||
if tc.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.expectedError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractNamespacesFromBackup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
backupResources map[string]*archive.ResourceItems
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "Multiple namespaces in backup",
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"ns1": {}, "ns2": {}, "ns3": {}}},
|
||||
},
|
||||
expected: []string{"ns1", "ns2", "ns3"},
|
||||
},
|
||||
{
|
||||
name: "Namespaces with resources",
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"app1": {}, "app2": {}}},
|
||||
"pods.v1": {ItemsByNamespace: map[string][]string{"app1": {"pod1"}}},
|
||||
"services.v1": {ItemsByNamespace: map[string][]string{"app2": {"svc1"}}},
|
||||
},
|
||||
expected: []string{"app1", "app2"},
|
||||
},
|
||||
{
|
||||
name: "Mixed cluster and namespaced resources",
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"test": {}}},
|
||||
"persistentvolumes": {ItemsByNamespace: map[string][]string{"": {"pv1"}}},
|
||||
"clusterroles": {ItemsByNamespace: map[string][]string{"": {"cr1"}}},
|
||||
"pods.v1": {ItemsByNamespace: map[string][]string{"test": {"pod1"}}},
|
||||
},
|
||||
expected: []string{"test"},
|
||||
},
|
||||
{
|
||||
name: "Empty backup",
|
||||
backupResources: map[string]*archive.ResourceItems{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "Only cluster resources",
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"persistentvolumes": {ItemsByNamespace: map[string][]string{"": {"pv1"}}},
|
||||
"clusterroles": {ItemsByNamespace: map[string][]string{"": {"cr1"}}},
|
||||
"storageclasses": {ItemsByNamespace: map[string][]string{"": {"sc1"}}},
|
||||
},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "Duplicate namespace entries",
|
||||
backupResources: map[string]*archive.ResourceItems{
|
||||
"namespaces": {ItemsByNamespace: map[string][]string{"app": {}}},
|
||||
"pods.v1": {ItemsByNamespace: map[string][]string{"app": {"pod1"}}},
|
||||
"configmaps.v1": {ItemsByNamespace: map[string][]string{"app": {"cm1"}}},
|
||||
},
|
||||
expected: []string{"app"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := extractNamespacesFromBackup(tc.backupResources)
|
||||
assert.ElementsMatch(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/pkg/discovery"
|
||||
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/wildcard"
|
||||
)
|
||||
|
||||
type globStringSet struct {
|
||||
@@ -55,6 +56,143 @@ func (gss globStringSet) match(match string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// NamespaceIncludesExcludes adds some features to IncludesExcludes
|
||||
// to handle namespace-specific functionality. In particular, it
|
||||
// provides a way to list all namespaces included in order to determine
|
||||
// overlap between backups, and it will be expanded in the future to
|
||||
// handle namespace wildcard values
|
||||
type NamespaceIncludesExcludes struct {
|
||||
activeNamespaces []string
|
||||
includesExcludes *IncludesExcludes
|
||||
wildcardExpanded bool
|
||||
wildcardResult []string
|
||||
}
|
||||
|
||||
func NewNamespaceIncludesExcludes() *NamespaceIncludesExcludes {
|
||||
return &NamespaceIncludesExcludes{
|
||||
activeNamespaces: []string{},
|
||||
includesExcludes: NewIncludesExcludes(),
|
||||
}
|
||||
}
|
||||
|
||||
func (nie *NamespaceIncludesExcludes) ActiveNamespaces(activeNamespaces []string) *NamespaceIncludesExcludes {
|
||||
nie.activeNamespaces = activeNamespaces
|
||||
return nie
|
||||
}
|
||||
|
||||
func (nie *NamespaceIncludesExcludes) IsWildcardExpanded() bool {
|
||||
return nie.wildcardExpanded
|
||||
}
|
||||
|
||||
// Includes adds items to the includes list. '*' is a wildcard
|
||||
// value meaning "include everything".
|
||||
func (nie *NamespaceIncludesExcludes) Includes(includes ...string) *NamespaceIncludesExcludes {
|
||||
nie.includesExcludes.Includes(includes...)
|
||||
return nie
|
||||
}
|
||||
|
||||
// GetIncludes returns the items in the includes list
|
||||
func (nie *NamespaceIncludesExcludes) GetIncludes() []string {
|
||||
return nie.includesExcludes.GetIncludes()
|
||||
}
|
||||
|
||||
func (nie *NamespaceIncludesExcludes) GetExcludes() []string {
|
||||
return nie.includesExcludes.GetExcludes()
|
||||
}
|
||||
|
||||
// SetIncludes sets the includes list to the given list
|
||||
func (nie *NamespaceIncludesExcludes) SetIncludes(includes []string) *NamespaceIncludesExcludes {
|
||||
nie.includesExcludes.includes = newGlobStringSet()
|
||||
nie.includesExcludes.includes.Insert(includes...)
|
||||
return nie
|
||||
}
|
||||
|
||||
// SetExcludes sets the excludes list to the given list
|
||||
func (nie *NamespaceIncludesExcludes) SetExcludes(excludes []string) *NamespaceIncludesExcludes {
|
||||
nie.includesExcludes.excludes = newGlobStringSet()
|
||||
nie.includesExcludes.excludes.Insert(excludes...)
|
||||
return nie
|
||||
}
|
||||
|
||||
// IncludesString returns a string containing all of the includes, separated by commas, or * if the
|
||||
// list is empty.
|
||||
func (nie *NamespaceIncludesExcludes) IncludesString() string {
|
||||
return nie.includesExcludes.IncludesString()
|
||||
}
|
||||
|
||||
// Excludes adds items to the includes list. '*' is a wildcard
|
||||
// value meaning "include everything".
|
||||
func (nie *NamespaceIncludesExcludes) Excludes(excludes ...string) *NamespaceIncludesExcludes {
|
||||
nie.includesExcludes.Excludes(excludes...)
|
||||
return nie
|
||||
}
|
||||
|
||||
// IncludesString returns a string containing all of the excludes, separated by commas, or * if the
|
||||
// list is empty.
|
||||
func (nie *NamespaceIncludesExcludes) ExcludesString() string {
|
||||
return nie.includesExcludes.ExcludesString()
|
||||
}
|
||||
|
||||
// ShouldInclude returns whether the specified item should be
|
||||
// included or not. Everything in the includes list except those
|
||||
// items in the excludes list should be included.
|
||||
func (nie *NamespaceIncludesExcludes) ShouldInclude(s string) bool {
|
||||
// Special case: if wildcard expansion occurred and resulted in an empty includes list,
|
||||
// it means the wildcard pattern matched nothing, so we should include nothing.
|
||||
// This differs from the default behavior where an empty includes list means "include everything".
|
||||
if nie.wildcardExpanded && nie.includesExcludes.includes.Len() == 0 {
|
||||
return false
|
||||
}
|
||||
return nie.includesExcludes.ShouldInclude(s)
|
||||
}
|
||||
|
||||
// IncludeEverything returns true if the includes list is empty or '*'
|
||||
// and the excludes list is empty, or false otherwise.
|
||||
func (nie *NamespaceIncludesExcludes) IncludeEverything() bool {
|
||||
return nie.includesExcludes.IncludeEverything()
|
||||
}
|
||||
|
||||
// Attempts to expand wildcard patterns, if any, in the includes and excludes lists.
|
||||
func (nie *NamespaceIncludesExcludes) ExpandIncludesExcludes() error {
|
||||
includes := nie.GetIncludes()
|
||||
excludes := nie.GetExcludes()
|
||||
|
||||
if wildcard.ShouldExpandWildcards(includes, excludes) {
|
||||
expandedIncludes, expandedExcludes, err := wildcard.ExpandWildcards(
|
||||
nie.activeNamespaces, includes, excludes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nie.SetIncludes(expandedIncludes)
|
||||
nie.SetExcludes(expandedExcludes)
|
||||
nie.wildcardExpanded = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveNamespaceList returns a list of all namespaces which will be backed up.
|
||||
// The second return value indicates whether wildcard expansion was performed.
|
||||
func (nie *NamespaceIncludesExcludes) ResolveNamespaceList() ([]string, error) {
|
||||
// Check if this is being called by non-backup processing e.g. backup queue controller
|
||||
if !nie.wildcardExpanded {
|
||||
err := nie.ExpandIncludesExcludes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
outNamespaces := []string{}
|
||||
for _, ns := range nie.activeNamespaces {
|
||||
if nie.ShouldInclude(ns) {
|
||||
outNamespaces = append(outNamespaces, ns)
|
||||
}
|
||||
}
|
||||
nie.wildcardResult = outNamespaces
|
||||
return nie.wildcardResult, nil
|
||||
}
|
||||
|
||||
// IncludesExcludes is a type that manages lists of included
|
||||
// and excluded items. The logic implemented is that everything
|
||||
// in the included list except those items in the excluded list
|
||||
@@ -171,7 +309,7 @@ type IncludesExcludesInterface interface {
|
||||
type GlobalIncludesExcludes struct {
|
||||
resourceFilter IncludesExcludes
|
||||
includeClusterResources *bool
|
||||
namespaceFilter IncludesExcludes
|
||||
namespaceFilter NamespaceIncludesExcludes
|
||||
|
||||
helper discovery.Helper
|
||||
logger logrus.FieldLogger
|
||||
@@ -239,7 +377,7 @@ func (ie *GlobalIncludesExcludes) ShouldExclude(typeName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func GetGlobalResourceIncludesExcludes(helper discovery.Helper, logger logrus.FieldLogger, includes, excludes []string, includeClusterResources *bool, nsIncludesExcludes IncludesExcludes) *GlobalIncludesExcludes {
|
||||
func GetGlobalResourceIncludesExcludes(helper discovery.Helper, logger logrus.FieldLogger, includes, excludes []string, includeClusterResources *bool, nsIncludesExcludes NamespaceIncludesExcludes) *GlobalIncludesExcludes {
|
||||
ret := &GlobalIncludesExcludes{
|
||||
resourceFilter: *GetResourceIncludesExcludes(helper, includes, excludes),
|
||||
includeClusterResources: includeClusterResources,
|
||||
@@ -256,7 +394,7 @@ func GetGlobalResourceIncludesExcludes(helper discovery.Helper, logger logrus.Fi
|
||||
type ScopeIncludesExcludes struct {
|
||||
namespaceScopedResourceFilter IncludesExcludes // namespace-scoped resource filter
|
||||
clusterScopedResourceFilter IncludesExcludes // cluster-scoped resource filter
|
||||
namespaceFilter IncludesExcludes // namespace filter
|
||||
namespaceFilter NamespaceIncludesExcludes // namespace filter
|
||||
|
||||
helper discovery.Helper
|
||||
logger logrus.FieldLogger
|
||||
@@ -384,7 +522,7 @@ func (ie *ScopeIncludesExcludes) CombineWithPolicy(policy *resourcepolicies.Incl
|
||||
ie.logger.Infof("Excluding cluster-scoped resources: %s", ie.clusterScopedResourceFilter.ExcludesString())
|
||||
}
|
||||
|
||||
func newScopeIncludesExcludes(nsIncludesExcludes IncludesExcludes, helper discovery.Helper, logger logrus.FieldLogger) *ScopeIncludesExcludes {
|
||||
func newScopeIncludesExcludes(nsIncludesExcludes NamespaceIncludesExcludes, helper discovery.Helper, logger logrus.FieldLogger) *ScopeIncludesExcludes {
|
||||
ret := &ScopeIncludesExcludes{
|
||||
namespaceScopedResourceFilter: IncludesExcludes{
|
||||
includes: newGlobStringSet(),
|
||||
@@ -404,7 +542,7 @@ func newScopeIncludesExcludes(nsIncludesExcludes IncludesExcludes, helper discov
|
||||
|
||||
// GetScopeResourceIncludesExcludes function is similar with GetResourceIncludesExcludes,
|
||||
// but it's used for scoped Includes/Excludes, and can handle both cluster-scoped and namespace-scoped resources.
|
||||
func GetScopeResourceIncludesExcludes(helper discovery.Helper, logger logrus.FieldLogger, namespaceIncludes, namespaceExcludes, clusterIncludes, clusterExcludes []string, nsIncludesExcludes IncludesExcludes) *ScopeIncludesExcludes {
|
||||
func GetScopeResourceIncludesExcludes(helper discovery.Helper, logger logrus.FieldLogger, namespaceIncludes, namespaceExcludes, clusterIncludes, clusterExcludes []string, nsIncludesExcludes NamespaceIncludesExcludes) *ScopeIncludesExcludes {
|
||||
ret := generateScopedIncludesExcludes(
|
||||
namespaceIncludes,
|
||||
namespaceExcludes,
|
||||
@@ -581,7 +719,7 @@ func generateIncludesExcludes(includes, excludes []string, mapFunc func(string)
|
||||
|
||||
// generateScopedIncludesExcludes function is similar with generateIncludesExcludes,
|
||||
// but it's used for scoped Includes/Excludes.
|
||||
func generateScopedIncludesExcludes(namespacedIncludes, namespacedExcludes, clusterIncludes, clusterExcludes []string, mapFunc func(string, bool) string, nsIncludesExcludes IncludesExcludes, helper discovery.Helper, logger logrus.FieldLogger) *ScopeIncludesExcludes {
|
||||
func generateScopedIncludesExcludes(namespacedIncludes, namespacedExcludes, clusterIncludes, clusterExcludes []string, mapFunc func(string, bool) string, nsIncludesExcludes NamespaceIncludesExcludes, helper discovery.Helper, logger logrus.FieldLogger) *ScopeIncludesExcludes {
|
||||
res := newScopeIncludesExcludes(nsIncludesExcludes, helper, logger)
|
||||
|
||||
generateFilter(res.namespaceScopedResourceFilter.includes, namespacedIncludes, mapFunc, true)
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/builder"
|
||||
@@ -503,7 +504,7 @@ func TestNamespaceScopedShouldInclude(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
discoveryHelper := setupDiscoveryClientWithResources(tc.apiResources)
|
||||
logger := logrus.StandardLogger()
|
||||
scopeIncludesExcludes := GetScopeResourceIncludesExcludes(discoveryHelper, logger, tc.namespaceScopedIncludes, tc.namespaceScopedExcludes, []string{}, []string{}, *NewIncludesExcludes())
|
||||
scopeIncludesExcludes := GetScopeResourceIncludesExcludes(discoveryHelper, logger, tc.namespaceScopedIncludes, tc.namespaceScopedExcludes, []string{}, []string{}, *NewNamespaceIncludesExcludes())
|
||||
|
||||
if got := scopeIncludesExcludes.ShouldInclude((tc.item)); got != tc.want {
|
||||
t.Errorf("want %t, got %t", tc.want, got)
|
||||
@@ -676,7 +677,7 @@ func TestClusterScopedShouldInclude(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
discoveryHelper := setupDiscoveryClientWithResources(tc.apiResources)
|
||||
logger := logrus.StandardLogger()
|
||||
nsIncludeExclude := NewIncludesExcludes().Includes(tc.nsIncludes...)
|
||||
nsIncludeExclude := NewNamespaceIncludesExcludes().Includes(tc.nsIncludes...)
|
||||
scopeIncludesExcludes := GetScopeResourceIncludesExcludes(discoveryHelper, logger, []string{}, []string{}, tc.clusterScopedIncludes, tc.clusterScopedExcludes, *nsIncludeExclude)
|
||||
|
||||
if got := scopeIncludesExcludes.ShouldInclude((tc.item)); got != tc.want {
|
||||
@@ -732,7 +733,7 @@ func TestGetScopedResourceIncludesExcludes(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
logger := logrus.StandardLogger()
|
||||
nsIncludeExclude := NewIncludesExcludes()
|
||||
nsIncludeExclude := NewNamespaceIncludesExcludes()
|
||||
resources := GetScopeResourceIncludesExcludes(setupDiscoveryClientWithResources(tc.apiResources), logger, tc.namespaceScopedIncludes, tc.namespaceScopedExcludes, tc.clusterScopedIncludes, tc.clusterScopedExcludes, *nsIncludeExclude)
|
||||
|
||||
assert.Equal(t, tc.expectedNamespaceScopedIncludes, resources.namespaceScopedResourceFilter.includes.List())
|
||||
@@ -830,7 +831,7 @@ func TestScopeIncludesExcludes_CombineWithPolicy(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
logger := logrus.StandardLogger()
|
||||
discoveryHelper := setupDiscoveryClientWithResources(apiResources)
|
||||
sie := GetScopeResourceIncludesExcludes(discoveryHelper, logger, tc.namespaceScopedIncludes, tc.namespaceScopedExcludes, tc.clusterScopedIncludes, tc.clusterScopedExcludes, *NewIncludesExcludes())
|
||||
sie := GetScopeResourceIncludesExcludes(discoveryHelper, logger, tc.namespaceScopedIncludes, tc.namespaceScopedExcludes, tc.clusterScopedIncludes, tc.clusterScopedExcludes, *NewNamespaceIncludesExcludes())
|
||||
sie.CombineWithPolicy(tc.policy)
|
||||
assert.True(t, tc.verify(*sie))
|
||||
})
|
||||
@@ -981,15 +982,391 @@ func TestShouldExcluded(t *testing.T) {
|
||||
|
||||
var ie IncludesExcludesInterface
|
||||
if tc.filterType == "global" {
|
||||
ie = GetGlobalResourceIncludesExcludes(setupDiscoveryClientWithResources(tc.apiResources), logger, tc.clusterIncludes, tc.clusterExcludes, tc.includeClusterResources, *NewIncludesExcludes())
|
||||
ie = GetGlobalResourceIncludesExcludes(setupDiscoveryClientWithResources(tc.apiResources), logger, tc.clusterIncludes, tc.clusterExcludes, tc.includeClusterResources, *NewNamespaceIncludesExcludes())
|
||||
} else if tc.filterType == "scope" {
|
||||
ie = GetScopeResourceIncludesExcludes(setupDiscoveryClientWithResources(tc.apiResources), logger, []string{}, []string{}, tc.clusterIncludes, tc.clusterExcludes, *NewIncludesExcludes())
|
||||
ie = GetScopeResourceIncludesExcludes(setupDiscoveryClientWithResources(tc.apiResources), logger, []string{}, []string{}, tc.clusterIncludes, tc.clusterExcludes, *NewNamespaceIncludesExcludes())
|
||||
}
|
||||
assert.Equal(t, tc.resourceIsExcluded, ie.ShouldExclude(tc.resourceName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandIncludesExcludes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
includes []string
|
||||
excludes []string
|
||||
activeNamespaces []string
|
||||
expectedIncludes []string
|
||||
expectedExcludes []string
|
||||
expectedWildcardExpanded bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "no wildcards - should not expand",
|
||||
includes: []string{"default", "kube-system"},
|
||||
excludes: []string{"kube-public"},
|
||||
activeNamespaces: []string{"default", "kube-system", "kube-public", "test"},
|
||||
expectedIncludes: []string{"default", "kube-system"},
|
||||
expectedExcludes: []string{"kube-public"},
|
||||
expectedWildcardExpanded: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "asterisk alone - should not expand",
|
||||
includes: []string{"*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "test"},
|
||||
expectedIncludes: []string{"*"},
|
||||
expectedExcludes: []string{},
|
||||
expectedWildcardExpanded: false,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard in includes - should expand",
|
||||
includes: []string{"kube-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "kube-public", "test"},
|
||||
expectedIncludes: []string{"kube-system", "kube-public"},
|
||||
expectedExcludes: []string{},
|
||||
expectedWildcardExpanded: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard in excludes - should expand",
|
||||
includes: []string{"default"},
|
||||
excludes: []string{"*-test"},
|
||||
activeNamespaces: []string{"default", "kube-test", "app-test", "prod"},
|
||||
expectedIncludes: []string{"default"},
|
||||
expectedExcludes: []string{"kube-test", "app-test"},
|
||||
expectedWildcardExpanded: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wildcards in both includes and excludes",
|
||||
includes: []string{"kube-*", "app-*"},
|
||||
excludes: []string{"*-test"},
|
||||
activeNamespaces: []string{"kube-system", "kube-test", "app-prod", "app-test", "default"},
|
||||
expectedIncludes: []string{"kube-system", "kube-test", "app-prod", "app-test"},
|
||||
expectedExcludes: []string{"kube-test", "app-test"},
|
||||
expectedWildcardExpanded: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard pattern matches nothing",
|
||||
includes: []string{"nonexistent-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system"},
|
||||
expectedIncludes: []string{},
|
||||
expectedExcludes: []string{},
|
||||
expectedWildcardExpanded: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "mix of wildcards and non-wildcards in includes",
|
||||
includes: []string{"default", "kube-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "kube-public", "test"},
|
||||
expectedIncludes: []string{"default", "kube-system", "kube-public"},
|
||||
expectedExcludes: []string{},
|
||||
expectedWildcardExpanded: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "question mark wildcard",
|
||||
includes: []string{"test-?"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"test-1", "test-2", "test-10", "default"},
|
||||
expectedIncludes: []string{"test-1", "test-2"},
|
||||
expectedExcludes: []string{},
|
||||
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-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{},
|
||||
expectedIncludes: []string{},
|
||||
expectedExcludes: []string{},
|
||||
expectedWildcardExpanded: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid wildcard pattern - consecutive asterisks",
|
||||
includes: []string{"kube-**"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default"},
|
||||
expectedIncludes: []string{"kube-**"},
|
||||
expectedExcludes: []string{},
|
||||
expectedWildcardExpanded: false,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nie := NewNamespaceIncludesExcludes().
|
||||
ActiveNamespaces(tc.activeNamespaces).
|
||||
Includes(tc.includes...).
|
||||
Excludes(tc.excludes...)
|
||||
|
||||
err := nie.ExpandIncludesExcludes()
|
||||
|
||||
if tc.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedWildcardExpanded, nie.IsWildcardExpanded())
|
||||
|
||||
// Check includes - convert to sets for order-independent comparison
|
||||
actualIncludes := sets.NewString(nie.GetIncludes()...)
|
||||
expectedIncludes := sets.NewString(tc.expectedIncludes...)
|
||||
assert.True(t, actualIncludes.Equal(expectedIncludes),
|
||||
"includes mismatch: expected %v, got %v", tc.expectedIncludes, nie.GetIncludes())
|
||||
|
||||
// Check excludes
|
||||
actualExcludes := sets.NewString(nie.GetExcludes()...)
|
||||
expectedExcludes := sets.NewString(tc.expectedExcludes...)
|
||||
assert.True(t, actualExcludes.Equal(expectedExcludes),
|
||||
"excludes mismatch: expected %v, got %v", tc.expectedExcludes, nie.GetExcludes())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNamespaceList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
includes []string
|
||||
excludes []string
|
||||
activeNamespaces []string
|
||||
expectedNamespaces []string
|
||||
preExpandWildcards bool
|
||||
}{
|
||||
{
|
||||
name: "no includes/excludes - all active namespaces",
|
||||
includes: []string{},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "test"},
|
||||
expectedNamespaces: []string{"default", "kube-system", "test"},
|
||||
},
|
||||
{
|
||||
name: "asterisk includes - all active namespaces",
|
||||
includes: []string{"*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "test"},
|
||||
expectedNamespaces: []string{"default", "kube-system", "test"},
|
||||
},
|
||||
{
|
||||
name: "specific includes - only those namespaces",
|
||||
includes: []string{"default", "test"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "test"},
|
||||
expectedNamespaces: []string{"default", "test"},
|
||||
},
|
||||
{
|
||||
name: "includes with excludes",
|
||||
includes: []string{"*"},
|
||||
excludes: []string{"kube-system"},
|
||||
activeNamespaces: []string{"default", "kube-system", "test"},
|
||||
expectedNamespaces: []string{"default", "test"},
|
||||
},
|
||||
{
|
||||
name: "wildcard includes - expands and filters",
|
||||
includes: []string{"kube-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "kube-public", "test"},
|
||||
expectedNamespaces: []string{"kube-system", "kube-public"},
|
||||
},
|
||||
{
|
||||
name: "wildcard includes with wildcard excludes",
|
||||
includes: []string{"app-*"},
|
||||
excludes: []string{"*-test"},
|
||||
activeNamespaces: []string{"app-prod", "app-dev", "app-test", "default"},
|
||||
expectedNamespaces: []string{"app-prod", "app-dev"},
|
||||
},
|
||||
{
|
||||
name: "wildcard matches nothing - empty result",
|
||||
includes: []string{"nonexistent-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system"},
|
||||
expectedNamespaces: []string{},
|
||||
},
|
||||
{
|
||||
name: "empty active namespaces",
|
||||
includes: []string{"*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{},
|
||||
expectedNamespaces: []string{},
|
||||
},
|
||||
{
|
||||
name: "includes namespace not in active namespaces",
|
||||
includes: []string{"default", "nonexistent"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "test"},
|
||||
expectedNamespaces: []string{"default"},
|
||||
},
|
||||
{
|
||||
name: "excludes all namespaces from includes",
|
||||
includes: []string{"default", "test"},
|
||||
excludes: []string{"default", "test"},
|
||||
activeNamespaces: []string{"default", "test", "prod"},
|
||||
expectedNamespaces: []string{},
|
||||
},
|
||||
{
|
||||
name: "pre-expanded wildcards - should not expand again",
|
||||
includes: []string{"kube-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "kube-public"},
|
||||
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-?"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"ns-1", "ns-2", "ns-10", "default"},
|
||||
expectedNamespaces: []string{"ns-1", "ns-2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nie := NewNamespaceIncludesExcludes().
|
||||
ActiveNamespaces(tc.activeNamespaces).
|
||||
Includes(tc.includes...).
|
||||
Excludes(tc.excludes...)
|
||||
|
||||
// Pre-expand wildcards if requested
|
||||
if tc.preExpandWildcards {
|
||||
err := nie.ExpandIncludesExcludes()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
namespaces, err := nie.ResolveNamespaceList()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Convert to sets for order-independent comparison
|
||||
actualNs := sets.NewString(namespaces...)
|
||||
expectedNs := sets.NewString(tc.expectedNamespaces...)
|
||||
assert.True(t, actualNs.Equal(expectedNs),
|
||||
"namespaces mismatch: expected %v, got %v", tc.expectedNamespaces, namespaces)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNamespaceListError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
includes []string
|
||||
excludes []string
|
||||
activeNamespaces []string
|
||||
}{
|
||||
{
|
||||
name: "invalid wildcard pattern in includes",
|
||||
includes: []string{"kube-**"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default"},
|
||||
},
|
||||
{
|
||||
name: "invalid wildcard pattern in excludes",
|
||||
includes: []string{"default"},
|
||||
excludes: []string{"test-**"},
|
||||
activeNamespaces: []string{"default"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nie := NewNamespaceIncludesExcludes().
|
||||
ActiveNamespaces(tc.activeNamespaces).
|
||||
Includes(tc.includes...).
|
||||
Excludes(tc.excludes...)
|
||||
|
||||
_, err := nie.ResolveNamespaceList()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceIncludesExcludesShouldIncludeAfterWildcardExpansion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
includes []string
|
||||
excludes []string
|
||||
activeNamespaces []string
|
||||
testNamespace string
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
name: "wildcard expanded to empty includes - should not include anything",
|
||||
includes: []string{"nonexistent-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system"},
|
||||
testNamespace: "default",
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard expanded with matches - should include matched namespace",
|
||||
includes: []string{"kube-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "kube-public"},
|
||||
testNamespace: "kube-system",
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard expanded with matches - should not include unmatched namespace",
|
||||
includes: []string{"kube-*"},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system", "kube-public"},
|
||||
testNamespace: "default",
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "no wildcard expansion - empty includes means include all",
|
||||
includes: []string{},
|
||||
excludes: []string{},
|
||||
activeNamespaces: []string{"default", "kube-system"},
|
||||
testNamespace: "default",
|
||||
expectedResult: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nie := NewNamespaceIncludesExcludes().
|
||||
ActiveNamespaces(tc.activeNamespaces).
|
||||
Includes(tc.includes...).
|
||||
Excludes(tc.excludes...)
|
||||
|
||||
err := nie.ExpandIncludesExcludes()
|
||||
require.NoError(t, err)
|
||||
|
||||
result := nie.ShouldInclude(tc.testNamespace)
|
||||
assert.Equal(t, tc.expectedResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupDiscoveryClientWithResources(APIResources []*test.APIResource) *test.FakeDiscoveryHelper {
|
||||
resourcesMap := make(map[schema.GroupVersionResource]schema.GroupVersionResource)
|
||||
resourceList := make([]*metav1.APIResourceList, 0)
|
||||
|
||||
175
pkg/util/wildcard/expand.go
Normal file
175
pkg/util/wildcard/expand.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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], {a,b,c}
|
||||
// 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 {
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateBracePatterns checks for malformed brace patterns like unclosed braces or empty braces
|
||||
func validateBracePatterns(pattern string) error {
|
||||
depth := 0
|
||||
|
||||
for i := 0; i < len(pattern); i++ {
|
||||
if pattern[i] == '{' {
|
||||
braceStart := i
|
||||
depth++
|
||||
|
||||
// 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 '{}'")
|
||||
}
|
||||
// Skip to the closing brace
|
||||
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 '{'")
|
||||
}
|
||||
|
||||
// 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 '}'")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
799
pkg/util/wildcard/expand_test.go
Normal file
799
pkg/util/wildcard/expand_test.go
Normal file
@@ -0,0 +1,799 @@
|
||||
package wildcard
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShouldExpandWildcards(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
includes []string
|
||||
excludes []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "no wildcards",
|
||||
includes: []string{"ns1", "ns2"},
|
||||
excludes: []string{"ns3", "ns4"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "includes has star - should not expand",
|
||||
includes: []string{"*"},
|
||||
excludes: []string{"ns1"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "includes has star after a wildcard pattern - should not expand",
|
||||
includes: []string{"ns*", "*"},
|
||||
excludes: []string{"ns1"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "includes has wildcard pattern",
|
||||
includes: []string{"ns*"},
|
||||
excludes: []string{"ns1"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "excludes has wildcard pattern",
|
||||
includes: []string{"ns1"},
|
||||
excludes: []string{"ns*"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "both have wildcard patterns",
|
||||
includes: []string{"app-*"},
|
||||
excludes: []string{"test-*"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "includes has star and wildcard - star takes precedence",
|
||||
includes: []string{"*", "ns*"},
|
||||
excludes: []string{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "double asterisk should be detected as wildcard",
|
||||
includes: []string{"**"},
|
||||
excludes: []string{},
|
||||
expected: true, // ** is a wildcard pattern (but will error during validation)
|
||||
},
|
||||
{
|
||||
name: "empty slices",
|
||||
includes: []string{},
|
||||
excludes: []string{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "complex wildcard patterns",
|
||||
includes: []string{"*-prod"},
|
||||
excludes: []string{"test-*-staging"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "question mark wildcard",
|
||||
includes: []string{"ns?"},
|
||||
excludes: []string{},
|
||||
expected: true, // question mark is now considered a wildcard
|
||||
},
|
||||
{
|
||||
name: "character class wildcard",
|
||||
includes: []string{"ns[abc]"},
|
||||
excludes: []string{},
|
||||
expected: true, // character class is considered wildcard
|
||||
},
|
||||
{
|
||||
name: "brace alternatives wildcard",
|
||||
includes: []string{"ns{prod,staging}"},
|
||||
excludes: []string{},
|
||||
expected: true, // brace alternatives are considered wildcard
|
||||
},
|
||||
{
|
||||
name: "dot is literal - not wildcard",
|
||||
includes: []string{"app.prod"},
|
||||
excludes: []string{},
|
||||
expected: false, // dot is literal, not wildcard
|
||||
},
|
||||
{
|
||||
name: "plus is literal - not wildcard",
|
||||
includes: []string{"app+"},
|
||||
excludes: []string{},
|
||||
expected: false, // plus is literal, not wildcard
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ShouldExpandWildcards(tt.includes, tt.excludes)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandWildcards(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
activeNamespaces []string
|
||||
includes []string
|
||||
excludes []string
|
||||
expectedIncludes []string
|
||||
expectedExcludes []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "no wildcards",
|
||||
activeNamespaces: []string{"ns1", "ns2", "ns3"},
|
||||
includes: []string{"ns1", "ns4"},
|
||||
excludes: []string{"ns2"},
|
||||
expectedIncludes: []string{"ns1", "ns4"},
|
||||
expectedExcludes: []string{"ns2"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard in includes",
|
||||
activeNamespaces: []string{"app-prod", "app-staging", "db-prod", "test-ns"},
|
||||
includes: []string{"app-*"},
|
||||
excludes: []string{"test-ns"},
|
||||
expectedIncludes: []string{"app-prod", "app-staging"},
|
||||
expectedExcludes: []string{"test-ns"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard in excludes",
|
||||
activeNamespaces: []string{"app-prod", "app-staging", "db-prod", "test-ns"},
|
||||
includes: []string{"app-prod"},
|
||||
excludes: []string{"*-staging"},
|
||||
expectedIncludes: []string{"app-prod"},
|
||||
expectedExcludes: []string{"app-staging"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wildcards in both",
|
||||
activeNamespaces: []string{"app-prod", "app-staging", "db-prod", "db-staging", "test-ns"},
|
||||
includes: []string{"*-prod"},
|
||||
excludes: []string{"*-staging"},
|
||||
expectedIncludes: []string{"app-prod", "db-prod"},
|
||||
expectedExcludes: []string{"app-staging", "db-staging"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "star pattern in includes",
|
||||
activeNamespaces: []string{"ns1", "ns2", "ns3"},
|
||||
includes: []string{"*"},
|
||||
excludes: []string{},
|
||||
expectedIncludes: []string{"ns1", "ns2", "ns3"},
|
||||
expectedExcludes: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty active namespaces",
|
||||
activeNamespaces: []string{},
|
||||
includes: []string{"app-*"},
|
||||
excludes: []string{"test-*"},
|
||||
expectedIncludes: nil,
|
||||
expectedExcludes: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty includes and excludes",
|
||||
activeNamespaces: []string{"ns1", "ns2"},
|
||||
includes: []string{},
|
||||
excludes: []string{},
|
||||
expectedIncludes: nil,
|
||||
expectedExcludes: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "complex patterns",
|
||||
activeNamespaces: []string{"my-app-prod", "my-app-staging", "your-app-prod", "system-ns"},
|
||||
includes: []string{"*-app-*"},
|
||||
excludes: []string{"*-staging"},
|
||||
expectedIncludes: []string{"my-app-prod", "my-app-staging", "your-app-prod"},
|
||||
expectedExcludes: []string{"my-app-staging"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "double asterisk should error",
|
||||
activeNamespaces: []string{"ns1", "ns2", "ns3"},
|
||||
includes: []string{"**"},
|
||||
excludes: []string{},
|
||||
expectedIncludes: nil,
|
||||
expectedExcludes: nil,
|
||||
expectError: true, // ** is invalid
|
||||
},
|
||||
{
|
||||
name: "double asterisk in pattern should error",
|
||||
activeNamespaces: []string{"ns1", "ns2", "ns3"},
|
||||
includes: []string{"app-**"},
|
||||
excludes: []string{},
|
||||
expectedIncludes: nil,
|
||||
expectedExcludes: nil,
|
||||
expectError: true, // app-** contains ** which is invalid
|
||||
},
|
||||
{
|
||||
name: "question mark patterns",
|
||||
activeNamespaces: []string{"ns1", "ns2", "ns10", "test"},
|
||||
includes: []string{"ns?"},
|
||||
excludes: []string{},
|
||||
expectedIncludes: []string{"ns1", "ns2"}, // ? matches single character
|
||||
expectedExcludes: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "character class patterns",
|
||||
activeNamespaces: []string{"nsa", "nsb", "nsc", "nsx", "ns1"},
|
||||
includes: []string{"ns[abc]"},
|
||||
excludes: []string{},
|
||||
expectedIncludes: []string{"nsa", "nsb", "nsc"}, // [abc] matches a, b, or c
|
||||
expectedExcludes: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "brace alternative patterns",
|
||||
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
|
||||
expectedExcludes: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "literal dot and plus patterns",
|
||||
activeNamespaces: []string{"app.prod", "app-prod", "app+", "app"},
|
||||
includes: []string{"app.prod", "app+"},
|
||||
excludes: []string{},
|
||||
expectedIncludes: []string{"app.prod", "app+"}, // . and + are literal
|
||||
expectedExcludes: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "unsupported regex patterns should error",
|
||||
activeNamespaces: []string{"ns1", "ns2"},
|
||||
includes: []string{"ns(1|2)"},
|
||||
excludes: []string{},
|
||||
expectedIncludes: nil,
|
||||
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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
includes, excludes, err := ExpandWildcards(tt.activeNamespaces, tt.includes, tt.excludes)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, tt.expectedIncludes, includes)
|
||||
assert.ElementsMatch(t, tt.expectedExcludes, excludes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandWildcardsPrivate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
activeNamespaces []string
|
||||
expected []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "empty patterns",
|
||||
patterns: []string{},
|
||||
activeNamespaces: []string{"ns1", "ns2"},
|
||||
expected: nil,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "non-wildcard patterns",
|
||||
patterns: []string{"ns1", "ns3"},
|
||||
activeNamespaces: []string{"ns1", "ns2"},
|
||||
expected: []string{"ns1", "ns3"}, // includes ns3 even if not in active
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "star pattern",
|
||||
patterns: []string{"*"},
|
||||
activeNamespaces: []string{"ns1", "ns2", "ns3"},
|
||||
expected: []string{"ns1", "ns2", "ns3"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "simple wildcard",
|
||||
patterns: []string{"app-*"},
|
||||
activeNamespaces: []string{"app-prod", "app-staging", "db-prod"},
|
||||
expected: []string{"app-prod", "app-staging"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "multiple patterns",
|
||||
patterns: []string{"app-*", "db-prod", "*-test"},
|
||||
activeNamespaces: []string{"app-prod", "app-staging", "db-prod", "service-test", "other"},
|
||||
expected: []string{"app-prod", "app-staging", "db-prod", "service-test"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard with no matches",
|
||||
patterns: []string{"missing-*"},
|
||||
activeNamespaces: []string{"app-prod", "db-staging"},
|
||||
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"},
|
||||
activeNamespaces: []string{"app-prod", "app-staging", "db-prod"},
|
||||
expected: []string{"app-prod", "app-staging", "db-prod"}, // no duplicates
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "question mark pattern - glob wildcard",
|
||||
patterns: []string{"ns?"},
|
||||
activeNamespaces: []string{"ns1", "ns2", "ns10"},
|
||||
expected: []string{"ns1", "ns2"}, // ? is a glob pattern for single character
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "character class patterns",
|
||||
patterns: []string{"ns[12]"},
|
||||
activeNamespaces: []string{"ns1", "ns2", "ns3", "nsa"},
|
||||
expected: []string{"ns1", "ns2"}, // [12] matches 1 or 2
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "character range patterns",
|
||||
patterns: []string{"ns[a-c]"},
|
||||
activeNamespaces: []string{"nsa", "nsb", "nsc", "nsd", "ns1"},
|
||||
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{"**"},
|
||||
activeNamespaces: []string{"app-prod", "app.staging", "db/prod"},
|
||||
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)"},
|
||||
activeNamespaces: []string{"ns1", "ns2"},
|
||||
expected: nil,
|
||||
expectError: true, // |, (, ) not supported
|
||||
},
|
||||
{
|
||||
name: "double asterisk should error",
|
||||
patterns: []string{"**"},
|
||||
activeNamespaces: []string{"ns1", "ns2"},
|
||||
expected: nil,
|
||||
expectError: true, // ** not allowed
|
||||
},
|
||||
{
|
||||
name: "double asterisk in pattern should error",
|
||||
patterns: []string{"app-**-prod"},
|
||||
activeNamespaces: []string{"app-prod"},
|
||||
expected: nil,
|
||||
expectError: true, // ** not allowed anywhere
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := expandWildcards(tt.patterns, tt.activeNamespaces)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
if tt.expected == nil {
|
||||
assert.Nil(t, result)
|
||||
} else if len(tt.expected) == 0 {
|
||||
assert.Empty(t, result)
|
||||
} else {
|
||||
assert.ElementsMatch(t, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBracePatterns(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern string
|
||||
expectError bool
|
||||
errorMsg string
|
||||
}{
|
||||
// Valid patterns
|
||||
{
|
||||
name: "valid single brace pattern",
|
||||
pattern: "app-{prod,staging}",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid brace with single option",
|
||||
pattern: "app-{prod}",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid brace with three options",
|
||||
pattern: "app-{prod,staging,dev}",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid pattern with text before and after brace",
|
||||
pattern: "prefix-{a,b}-suffix",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid pattern with no braces",
|
||||
pattern: "app-prod",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid pattern with asterisk",
|
||||
pattern: "app-*",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid brace with spaces around content",
|
||||
pattern: "app-{ prod , staging }",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid brace with numbers",
|
||||
pattern: "ns-{1,2,3}",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid brace with hyphens in options",
|
||||
pattern: "{app-prod,db-staging}",
|
||||
expectError: false,
|
||||
},
|
||||
|
||||
// Unclosed opening braces
|
||||
{
|
||||
name: "unclosed opening brace at end",
|
||||
pattern: "app-{prod,staging",
|
||||
expectError: true,
|
||||
errorMsg: "unclosed brace",
|
||||
},
|
||||
{
|
||||
name: "unclosed opening brace at start",
|
||||
pattern: "{prod,staging",
|
||||
expectError: true,
|
||||
errorMsg: "unclosed brace",
|
||||
},
|
||||
{
|
||||
name: "unclosed opening brace in middle",
|
||||
pattern: "app-{prod-test",
|
||||
expectError: true,
|
||||
errorMsg: "unclosed brace",
|
||||
},
|
||||
{
|
||||
name: "multiple unclosed braces",
|
||||
pattern: "app-{prod-{staging",
|
||||
expectError: true,
|
||||
errorMsg: "unclosed brace",
|
||||
},
|
||||
|
||||
// Unmatched closing braces
|
||||
{
|
||||
name: "unmatched closing brace at end",
|
||||
pattern: "app-prod}",
|
||||
expectError: true,
|
||||
errorMsg: "unmatched closing brace",
|
||||
},
|
||||
{
|
||||
name: "unmatched closing brace at start",
|
||||
pattern: "}app-prod",
|
||||
expectError: true,
|
||||
errorMsg: "unmatched closing brace",
|
||||
},
|
||||
{
|
||||
name: "unmatched closing brace in middle",
|
||||
pattern: "app-}prod",
|
||||
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",
|
||||
},
|
||||
|
||||
// Edge cases
|
||||
{
|
||||
name: "empty pattern",
|
||||
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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateBracePatterns(tt.pattern)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err, "Expected error for pattern: %s", tt.pattern)
|
||||
if tt.errorMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain: %s", tt.errorMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "Expected no error for pattern: %s", tt.pattern)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Edge case tests
|
||||
func TestExpandWildcardsEdgeCases(t *testing.T) {
|
||||
t.Run("nil inputs", func(t *testing.T) {
|
||||
includes, excludes, err := ExpandWildcards(nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, includes)
|
||||
assert.Nil(t, excludes)
|
||||
})
|
||||
|
||||
t.Run("empty string patterns", func(t *testing.T) {
|
||||
activeNamespaces := []string{"ns1", "ns2"}
|
||||
result, err := expandWildcards([]string{""}, activeNamespaces)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, []string{""}, result) // empty string is treated as literal
|
||||
})
|
||||
|
||||
t.Run("whitespace patterns", func(t *testing.T) {
|
||||
activeNamespaces := []string{"ns1", " ", "ns2"}
|
||||
result, err := expandWildcards([]string{" "}, activeNamespaces)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, []string{" "}, result)
|
||||
})
|
||||
|
||||
t.Run("special characters in namespace names", func(t *testing.T) {
|
||||
activeNamespaces := []string{"ns-1", "ns_2", "ns.3", "ns@4"}
|
||||
result, err := expandWildcards([]string{"ns*"}, activeNamespaces)
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, []string{"app.prod", "app-prod", "app_prod"}, result)
|
||||
})
|
||||
|
||||
t.Run("conservative asterisk validation", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern string
|
||||
shouldError bool
|
||||
}{
|
||||
{"single asterisk", "*", false},
|
||||
{"double asterisk", "**", true},
|
||||
{"triple asterisk", "***", true},
|
||||
{"quadruple asterisk", "****", true},
|
||||
{"mixed with double", "app-**", true},
|
||||
{"double in middle", "app-**-prod", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := expandWildcards([]string{tt.pattern}, []string{"test"})
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("malformed pattern validation", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern string
|
||||
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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := expandWildcards([]string{tt.pattern}, []string{"test"})
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err, "Expected error for pattern: %s", tt.pattern)
|
||||
} else {
|
||||
assert.NoError(t, err, "Expected no error for pattern: %s", tt.pattern)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -41,19 +41,23 @@ func (n *NamespaceMapping) Init() error {
|
||||
if n.UseVolumeSnapshots {
|
||||
backupType = "snapshot"
|
||||
}
|
||||
var mappedNS string
|
||||
var mappedNSSb strings.Builder
|
||||
var mappedNSList []string
|
||||
n.NSIncluded = &[]string{}
|
||||
|
||||
for nsNum := 0; nsNum < n.NamespacesTotal; nsNum++ {
|
||||
if nsNum > 0 {
|
||||
mappedNSSb.WriteString(",")
|
||||
}
|
||||
createNSName := fmt.Sprintf("%s-%00000d", n.CaseBaseName, nsNum)
|
||||
*n.NSIncluded = append(*n.NSIncluded, createNSName)
|
||||
mappedNS = mappedNS + createNSName + ":" + createNSName + "-mapped"
|
||||
mappedNSSb.WriteString(createNSName)
|
||||
mappedNSSb.WriteString(":")
|
||||
mappedNSSb.WriteString(createNSName)
|
||||
mappedNSSb.WriteString("-mapped")
|
||||
mappedNSList = append(mappedNSList, createNSName+"-mapped")
|
||||
mappedNS = mappedNS + ","
|
||||
}
|
||||
mappedNS = strings.TrimRightFunc(mappedNS, func(r rune) bool {
|
||||
return r == ','
|
||||
})
|
||||
mappedNS := mappedNSSb.String()
|
||||
|
||||
n.TestMsg = &TestMSG{
|
||||
Desc: fmt.Sprintf("Restore namespace %s with namespace mapping by %s test", *n.NSIncluded, backupType),
|
||||
|
||||
Reference in New Issue
Block a user