From 343dec049aaeb75cadc78aa87a8fdac7e56a6db5 Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 08:53:48 -0400 Subject: [PATCH] feat(sourceNamespace): Regex Support (#19016) (#19017) (#19664) * feat(sourceNamespace): Regex Support * feat(sourceNamespace): Separate exactMatch into patternMatch --------- Signed-off-by: Arthur Co-authored-by: Arthur Vardevanyan --- .../controllers/applicationset_controller.go | 2 +- controller/appcontroller.go | 2 +- docs/operator-manual/app-any-namespace.md | 5 ++- go.mod | 1 + go.sum | 2 ++ .../controller/controller.go | 4 +-- .../application/v1alpha1/app_project_types.go | 2 +- util/argo/argo.go | 4 +-- util/glob/glob_test.go | 32 ++++++++++--------- util/glob/list.go | 28 +++++++++++++--- util/regex/regex.go | 20 ++++++++++++ util/security/application_namespaces.go | 2 +- util/security/application_namespaces_test.go | 14 ++++++++ util/webhook/webhook.go | 2 +- 14 files changed, 91 insertions(+), 29 deletions(-) create mode 100644 util/regex/regex.go diff --git a/applicationset/controllers/applicationset_controller.go b/applicationset/controllers/applicationset_controller.go index 6e4b50060e1ec..c6a07b394822b 100644 --- a/applicationset/controllers/applicationset_controller.go +++ b/applicationset/controllers/applicationset_controller.go @@ -580,7 +580,7 @@ func (r *ApplicationSetReconciler) applyTemplatePatch(app *argov1alpha1.Applicat func ignoreNotAllowedNamespaces(namespaces []string) predicate.Predicate { return predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { - return glob.MatchStringInList(namespaces, e.Object.GetNamespace(), false) + return glob.MatchStringInList(namespaces, e.Object.GetNamespace(), glob.GLOB) }, } } diff --git a/controller/appcontroller.go b/controller/appcontroller.go index bf5630c0492fe..3db3ee91172a1 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -2011,7 +2011,7 @@ func (ctrl *ApplicationController) shouldSelfHeal(app *appv1.Application) (bool, // isAppNamespaceAllowed returns whether the application is allowed in the // namespace it's residing in. func (ctrl *ApplicationController) isAppNamespaceAllowed(app *appv1.Application) bool { - return app.Namespace == ctrl.namespace || glob.MatchStringInList(ctrl.applicationNamespaces, app.Namespace, false) + return app.Namespace == ctrl.namespace || glob.MatchStringInList(ctrl.applicationNamespaces, app.Namespace, glob.GLOB) } func (ctrl *ApplicationController) canProcessApp(obj interface{}) bool { diff --git a/docs/operator-manual/app-any-namespace.md b/docs/operator-manual/app-any-namespace.md index dfd24f75b65f3..09df16e061314 100644 --- a/docs/operator-manual/app-any-namespace.md +++ b/docs/operator-manual/app-any-namespace.md @@ -42,8 +42,11 @@ In order for an application to be managed and reconciled outside the Argo CD's c In order to enable this feature, the Argo CD administrator must reconfigure the `argocd-server` and `argocd-application-controller` workloads to add the `--application-namespaces` parameter to the container's startup command. -The `--application-namespaces` parameter takes a comma-separated list of namespaces where `Applications` are to be allowed in. Each entry of the list supports shell-style wildcards such as `*`, so for example the entry `app-team-*` would match `app-team-one` and `app-team-two`. To enable all namespaces on the cluster where Argo CD is running on, you can just specify `*`, i.e. `--application-namespaces=*`. +The `--application-namespaces` parameter takes a comma-separated list of namespaces where `Applications` are to be allowed in. Each entry of the list supports: +- shell-style wildcards such as `*`, so for example the entry `app-team-*` would match `app-team-one` and `app-team-two`. To enable all namespaces on the cluster where Argo CD is running on, you can just specify `*`, i.e. `--application-namespaces=*`. +- regex, requires wrapping the string in ```/```, example to allow all namespaces except a particular one: ```/^((?!not-allowed).)*$/```. + The startup parameters for both, the `argocd-server` and the `argocd-application-controller` can also be conveniently set up and kept in sync by specifying the `application.namespaces` settings in the `argocd-cmd-params-cm` ConfigMap _instead_ of changing the manifests for the respective workloads. For example: ```yaml diff --git a/go.mod b/go.mod index 2e43481fd30b8..876dd5e33c076 100644 --- a/go.mod +++ b/go.mod @@ -187,6 +187,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dlclark/regexp2 v1.11.2 github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect diff --git a/go.sum b/go.sum index 308b3f22439e0..76f8890dc7833 100644 --- a/go.sum +++ b/go.sum @@ -843,6 +843,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.11.2 h1:/u628IuisSTwri5/UKloiIsH8+qF2Pu7xEQX+yIKg68= +github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= diff --git a/notification_controller/controller/controller.go b/notification_controller/controller/controller.go index 1bc3e73a6fbd7..796dbb7768f57 100644 --- a/notification_controller/controller/controller.go +++ b/notification_controller/controller/controller.go @@ -122,7 +122,7 @@ func NewController( // Check if app is not in the namespace where the controller is in, and also app is not in one of the applicationNamespaces func checkAppNotInAdditionalNamespaces(app *unstructured.Unstructured, namespace string, applicationNamespaces []string) bool { - return namespace != app.GetNamespace() && !glob.MatchStringInList(applicationNamespaces, app.GetNamespace(), false) + return namespace != app.GetNamespace() && !glob.MatchStringInList(applicationNamespaces, app.GetNamespace(), glob.GLOB) } func (c *notificationController) alterDestinations(obj v1.Object, destinations services.Destinations, cfg api.Config) services.Destinations { @@ -151,7 +151,7 @@ func newInformer(resClient dynamic.ResourceInterface, controllerNamespace string } newItems := []unstructured.Unstructured{} for _, res := range appList.Items { - if controllerNamespace == res.GetNamespace() || glob.MatchStringInList(applicationNamespaces, res.GetNamespace(), false) { + if controllerNamespace == res.GetNamespace() || glob.MatchStringInList(applicationNamespaces, res.GetNamespace(), glob.GLOB) { newItems = append(newItems, res) } } diff --git a/pkg/apis/application/v1alpha1/app_project_types.go b/pkg/apis/application/v1alpha1/app_project_types.go index 48a90d5a8233a..3adeb36520cce 100644 --- a/pkg/apis/application/v1alpha1/app_project_types.go +++ b/pkg/apis/application/v1alpha1/app_project_types.go @@ -562,5 +562,5 @@ func (p AppProject) IsAppNamespacePermitted(app *Application, controllerNs strin return true } - return glob.MatchStringInList(p.Spec.SourceNamespaces, app.Namespace, false) + return glob.MatchStringInList(p.Spec.SourceNamespaces, app.Namespace, glob.GLOB) } diff --git a/util/argo/argo.go b/util/argo/argo.go index 2afcce850e71a..bfdddf98143b1 100644 --- a/util/argo/argo.go +++ b/util/argo/argo.go @@ -1132,7 +1132,7 @@ func GetAppEventLabels(app *argoappv1.Application, projLister applicationsv1.App // Filter out event labels to include inKeys := settingsManager.GetIncludeEventLabelKeys() for k, v := range labels { - found := glob.MatchStringInList(inKeys, k, false) + found := glob.MatchStringInList(inKeys, k, glob.GLOB) if found { eventLabels[k] = v } @@ -1141,7 +1141,7 @@ func GetAppEventLabels(app *argoappv1.Application, projLister applicationsv1.App // Remove excluded event labels exKeys := settingsManager.GetExcludeEventLabelKeys() for k := range eventLabels { - found := glob.MatchStringInList(exKeys, k, false) + found := glob.MatchStringInList(exKeys, k, glob.GLOB) if found { delete(eventLabels, k) } diff --git a/util/glob/glob_test.go b/util/glob/glob_test.go index 79d74320361de..a0a3995382c92 100644 --- a/util/glob/glob_test.go +++ b/util/glob/glob_test.go @@ -31,26 +31,28 @@ func Test_Match(t *testing.T) { func Test_MatchList(t *testing.T) { tests := []struct { - name string - input string - list []string - exact bool - result bool + name string + input string + list []string + patternMatch string + result bool }{ - {"Exact name in list", "test", []string{"test"}, true, true}, - {"Exact name not in list", "test", []string{"other"}, true, false}, - {"Exact name not in list, multiple elements", "test", []string{"some", "other"}, true, false}, - {"Exact name not in list, list empty", "test", []string{}, true, false}, - {"Exact name not in list, empty element", "test", []string{""}, true, false}, - {"Glob name in list, but exact wanted", "test", []string{"*"}, true, false}, - {"Glob name in list with simple wildcard", "test", []string{"*"}, false, true}, - {"Glob name in list without wildcard", "test", []string{"test"}, false, true}, - {"Glob name in list, multiple elements", "test", []string{"other*", "te*"}, false, true}, + {"Exact name in list", "test", []string{"test"}, EXACT, true}, + {"Exact name not in list", "test", []string{"other"}, EXACT, false}, + {"Exact name not in list, multiple elements", "test", []string{"some", "other"}, EXACT, false}, + {"Exact name not in list, list empty", "test", []string{}, EXACT, false}, + {"Exact name not in list, empty element", "test", []string{""}, EXACT, false}, + {"Glob name in list, but exact wanted", "test", []string{"*"}, EXACT, false}, + {"Glob name in list with simple wildcard", "test", []string{"*"}, GLOB, true}, + {"Glob name in list without wildcard", "test", []string{"test"}, GLOB, true}, + {"Glob name in list, multiple elements", "test", []string{"other*", "te*"}, GLOB, true}, + {"match everything but specified word: fail", "disallowed", []string{"/^((?!disallowed).)*$/"}, REGEXP, false}, + {"match everything but specified word: pass", "allowed", []string{"/^((?!disallowed).)*$/"}, REGEXP, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - res := MatchStringInList(tt.list, tt.input, tt.exact) + res := MatchStringInList(tt.list, tt.input, tt.patternMatch) assert.Equal(t, tt.result, res) }) } diff --git a/util/glob/list.go b/util/glob/list.go index 1a6a8732ad3f8..f257302af9aa7 100644 --- a/util/glob/list.go +++ b/util/glob/list.go @@ -1,10 +1,30 @@ package glob -// MatchStringInList will return true if item is contained in list. If -// exactMatch is set to false, list may contain globs to be matched. -func MatchStringInList(list []string, item string, exactMatch bool) bool { +import ( + "strings" + + "github.com/argoproj/argo-cd/v2/util/regex" +) + +const ( + EXACT = "exact" + GLOB = "glob" + REGEXP = "regexp" +) + +// MatchStringInList will return true if item is contained in list. +// patternMatch; can be set to exact, glob, regexp. +// If patternMatch; is set to exact, the item must be an exact match. +// If patternMatch; is set to glob, the item must match a glob pattern. +// If patternMatch; is set to regexp, the item must match a regular expression or glob. +func MatchStringInList(list []string, item string, patternMatch string) bool { for _, ll := range list { - if item == ll || (!exactMatch && Match(ll, item)) { + // If string is wrapped in "/", assume it is a regular expression. + if patternMatch == REGEXP && strings.HasPrefix(ll, "/") && strings.HasSuffix(ll, "/") && regex.Match(ll[1:len(ll)-1], item) { + return true + } else if (patternMatch == REGEXP || patternMatch == GLOB) && Match(ll, item) { + return true + } else if patternMatch == EXACT && item == ll { return true } } diff --git a/util/regex/regex.go b/util/regex/regex.go new file mode 100644 index 0000000000000..9ff73b8497e0a --- /dev/null +++ b/util/regex/regex.go @@ -0,0 +1,20 @@ +package regex + +import ( + "github.com/dlclark/regexp2" + log "github.com/sirupsen/logrus" +) + +func Match(pattern, text string) bool { + compiledRegex, err := regexp2.Compile(pattern, 0) + if err != nil { + log.Warnf("failed to compile pattern %s due to error %v", pattern, err) + return false + } + regexMatch, err := compiledRegex.MatchString(text) + if err != nil { + log.Warnf("failed to match pattern %s due to error %v", pattern, err) + return false + } + return regexMatch +} diff --git a/util/security/application_namespaces.go b/util/security/application_namespaces.go index 2ef5edea33a7f..89019beb6f5a4 100644 --- a/util/security/application_namespaces.go +++ b/util/security/application_namespaces.go @@ -7,7 +7,7 @@ import ( ) func IsNamespaceEnabled(namespace string, serverNamespace string, enabledNamespaces []string) bool { - return namespace == serverNamespace || glob.MatchStringInList(enabledNamespaces, namespace, false) + return namespace == serverNamespace || glob.MatchStringInList(enabledNamespaces, namespace, glob.REGEXP) } func NamespaceNotPermittedError(namespace string) error { diff --git a/util/security/application_namespaces_test.go b/util/security/application_namespaces_test.go index a2a5292385db4..c6f3741ffe950 100644 --- a/util/security/application_namespaces_test.go +++ b/util/security/application_namespaces_test.go @@ -49,6 +49,20 @@ func Test_IsNamespaceEnabled(t *testing.T) { []string{"allowed"}, false, }, + { + "match everything but specified word: fail", + "disallowed", + "argocd", + []string{"/^((?!disallowed).)*$/"}, + false, + }, + { + "match everything but specified word: pass", + "allowed", + "argocd", + []string{"/^((?!disallowed).)*$/"}, + true, + }, } for _, tc := range testCases { diff --git a/util/webhook/webhook.go b/util/webhook/webhook.go index 65ed848ac4fd2..f9129476c5667 100644 --- a/util/webhook/webhook.go +++ b/util/webhook/webhook.go @@ -278,7 +278,7 @@ func (a *ArgoCDWebhookHandler) HandleEvent(payload interface{}) { // nor in the list of enabled namespaces. var filteredApps []v1alpha1.Application for _, app := range apps.Items { - if app.Namespace == a.ns || glob.MatchStringInList(a.appNs, app.Namespace, false) { + if app.Namespace == a.ns || glob.MatchStringInList(a.appNs, app.Namespace, glob.GLOB) { filteredApps = append(filteredApps, app) } }