From 26e4588edd627792602f2b1f13b70c2013b9ea9d Mon Sep 17 00:00:00 2001 From: Alex Fenlon Date: Mon, 15 Dec 2025 14:21:57 +0000 Subject: [PATCH 1/5] Add `nginx.org/app-root` annotation support --- internal/configs/annotations.go | 4 + internal/configs/config_params.go | 1 + internal/configs/ingress.go | 1 + internal/configs/ingress_test.go | 31 +++++++ internal/configs/version1/config.go | 2 + .../configs/version1/nginx-plus.ingress.tmpl | 6 ++ internal/configs/version1/nginx.ingress.tmpl | 6 ++ internal/k8s/validation.go | 38 +++++++++ internal/k8s/validation_test.go | 80 +++++++++++++++++++ internal/telemetry/collector_test.go | 30 +++++++ 10 files changed, 199 insertions(+) diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index 8e1f58ef85..4e8ea29e61 100644 --- a/internal/configs/annotations.go +++ b/internal/configs/annotations.go @@ -503,6 +503,10 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool } } + if appRoot, exists := ingEx.Ingress.Annotations["nginx.org/app-root"]; exists { + cfgParams.AppRoot = appRoot + } + if useClusterIP, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, UseClusterIPAnnotation, ingEx.Ingress); exists { if err != nil { nl.Error(l, err) diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go index 82e32944da..a477ad63be 100644 --- a/internal/configs/config_params.go +++ b/internal/configs/config_params.go @@ -10,6 +10,7 @@ import ( // ConfigParams holds NGINX configuration parameters that affect the main NGINX config // as well as configs for Ingress resources. type ConfigParams struct { + AppRoot string Context context.Context ClientMaxBodySize string ClientBodyBufferSize string diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go index 0b4fc2d12b..54b5e0ffad 100644 --- a/internal/configs/ingress.go +++ b/internal/configs/ingress.go @@ -184,6 +184,7 @@ func generateNginxCfg(p NginxCfgParams) (version1.IngressNginxConfig, Warnings) AppProtectLogEnable: cfgParams.AppProtectLogEnable, SpiffeCerts: cfgParams.SpiffeServerCerts, DisableIPV6: p.staticParams.DisableIPV6, + AppRoot: cfgParams.AppRoot, } warnings := addSSLConfig(&server, p.ingEx.Ingress, rule.Host, p.ingEx.Ingress.Spec.TLS, p.ingEx.SecretRefs, p.isWildcardEnabled) diff --git a/internal/configs/ingress_test.go b/internal/configs/ingress_test.go index 2e5ddb5f2e..cf051f6940 100644 --- a/internal/configs/ingress_test.go +++ b/internal/configs/ingress_test.go @@ -139,6 +139,37 @@ func TestGenerateNginxCfgForBasicAuth(t *testing.T) { } } +func TestGenerateNginxCfgForAppRoot(t *testing.T) { + t.Parallel() + cafeIngressEx := createCafeIngressEx() + cafeIngressEx.Ingress.Annotations["nginx.org/app-root"] = "/coffee" + + isPlus := false + configParams := NewDefaultConfigParams(context.Background(), isPlus) + + expected := createExpectedConfigForCafeIngressEx(isPlus) + expected.Servers[0].AppRoot = "/coffee" + + result, warnings := generateNginxCfg(NginxCfgParams{ + staticParams: &StaticConfigParams{}, + ingEx: &cafeIngressEx, + apResources: nil, + dosResource: nil, + isMinion: false, + isPlus: isPlus, + BaseCfgParams: configParams, + isResolverConfigured: false, + isWildcardEnabled: false, + }) + + if result.Servers[0].AppRoot != expected.Servers[0].AppRoot { + t.Errorf("generateNginxCfg returned AppRoot %v, but expected %v", result.Servers[0].AppRoot, expected.Servers[0].AppRoot) + } + if len(warnings) != 0 { + t.Errorf("generateNginxCfg returned warnings: %v", warnings) + } +} + func TestGenerateNginxCfgWithMissingTLSSecret(t *testing.T) { t.Parallel() cafeIngressEx := createCafeIngressEx() diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index 317157db97..fe8840245f 100644 --- a/internal/configs/version1/config.go +++ b/internal/configs/version1/config.go @@ -132,6 +132,8 @@ type Server struct { SpiffeCerts bool DisableIPV6 bool + + AppRoot string } // JWTRedirectLocation describes a location for redirecting client requests to a login URL for JWT Authentication. diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl index 25b2049e5d..cef055ac1f 100644 --- a/internal/configs/version1/nginx-plus.ingress.tmpl +++ b/internal/configs/version1/nginx-plus.ingress.tmpl @@ -172,6 +172,12 @@ server { {{$value}}{{end}} {{- end}} + {{ if $server.AppRoot }} + if ($uri = /) { + return 302 $scheme://$http_host{{ $server.AppRoot }}; + } + {{ end }} + {{- range $healthCheck := $server.HealthChecks}} location @hc-{{$healthCheck.UpstreamName}} { {{- range $name, $header := $healthCheck.Headers}} diff --git a/internal/configs/version1/nginx.ingress.tmpl b/internal/configs/version1/nginx.ingress.tmpl index 70d8f2cfa5..92aa55e221 100644 --- a/internal/configs/version1/nginx.ingress.tmpl +++ b/internal/configs/version1/nginx.ingress.tmpl @@ -116,6 +116,12 @@ server { {{- range $value := $server.ServerSnippets}} {{$value}}{{- end}} + {{ if $server.AppRoot }} + if ($uri = /) { + return 302 $scheme://$http_host{{ $server.AppRoot }}; + } + {{ end }} + {{- range $location := $server.Locations}} location {{ makeLocationPath $location $.Ingress.Annotations | printf }} { set $service "{{$location.ServiceName}}"; diff --git a/internal/k8s/validation.go b/internal/k8s/validation.go index ef2274d746..b02fa0a4a9 100644 --- a/internal/k8s/validation.go +++ b/internal/k8s/validation.go @@ -75,6 +75,7 @@ const ( stickyCookieServicesAnnotation = "nginx.com/sticky-cookie-services" pathRegexAnnotation = "nginx.org/path-regex" useClusterIPAnnotation = "nginx.org/use-cluster-ip" + appRootAnnotation = "nginx.org/app-root" ) const ( @@ -360,6 +361,9 @@ var ( useClusterIPAnnotation: { validateBoolAnnotation, }, + appRootAnnotation: { + validateAppRootAnnotation, + }, } annotationNames = sortedAnnotationNames(annotationValidations) ) @@ -373,6 +377,40 @@ func validatePathRegex(context *annotationValidationContext) field.ErrorList { } } +func validateAppRootAnnotation(context *annotationValidationContext) field.ErrorList { + allErrs := field.ErrorList{} + + path := context.value + + // App root must start with / + if !strings.HasPrefix(path, "/") { + allErrs = append(allErrs, field.Invalid(context.fieldPath, path, "must start with '/'")) + return allErrs + } + + // App root cannot be just "/" + if path == "/" { + allErrs = append(allErrs, field.Invalid(context.fieldPath, path, "cannot be '/'")) + return allErrs + } + + // Validate that the path doesn't contain invalid characters + // Allow alphanumeric, hyphens, underscores, dots, and forward slashes + validPath := regexp.MustCompile(`^/[a-zA-Z0-9\-_./]*$`) + if !validPath.MatchString(path) { + allErrs = append(allErrs, field.Invalid(context.fieldPath, path, "contains invalid characters, only alphanumeric, hyphens, underscores, dots, and forward slashes are allowed")) + return allErrs + } + + // Ensure path doesn't end with / + if strings.HasSuffix(path, "/") { + allErrs = append(allErrs, field.Invalid(context.fieldPath, path, "path should not end with '/'")) + return allErrs + } + + return allErrs +} + func validateJWTLoginURLAnnotation(context *annotationValidationContext) field.ErrorList { allErrs := field.ErrorList{} diff --git a/internal/k8s/validation_test.go b/internal/k8s/validation_test.go index 7e7403c5ea..b5c773af74 100644 --- a/internal/k8s/validation_test.go +++ b/internal/k8s/validation_test.go @@ -3465,6 +3465,86 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, msg: "invalid nginx.org/rewrite-target annotation, pipe character for alternatives", }, + { + annotations: map[string]string{ + "nginx.org/app-root": "/coffee", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + expectedErrors: nil, + msg: "valid nginx.org/app-root annotation", + }, + { + annotations: map[string]string{ + "nginx.org/app-root": "/coffee/mocha", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + expectedErrors: nil, + msg: "valid nginx.org/app-root annotation with nested path", + }, + { + annotations: map[string]string{ + "nginx.org/app-root": "coffee", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + expectedErrors: []string{ + `annotations.nginx.org/app-root: Invalid value: "coffee": must start with '/'`, + }, + msg: "invalid nginx.org/app-root annotation, does not start with slash", + }, + { + annotations: map[string]string{ + "nginx.org/app-root": "/", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + expectedErrors: []string{ + `annotations.nginx.org/app-root: Invalid value: "/": cannot be '/'`, + }, + msg: "invalid nginx.org/app-root annotation, cannot be root path", + }, + { + annotations: map[string]string{ + "nginx.org/app-root": "/coffee/", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + expectedErrors: []string{ + `annotations.nginx.org/app-root: Invalid value: "/coffee/": path should not end with '/'`, + }, + msg: "invalid nginx.org/app-root annotation, cannot end with slash", + }, + { + annotations: map[string]string{ + "nginx.org/app-root": "/tea$1", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + expectedErrors: []string{ + `annotations.nginx.org/app-root: Invalid value: "/tea$1": contains invalid characters, only alphanumeric, hyphens, underscores, dots, and forward slashes are allowed`, + }, + msg: "invalid nginx.org/app-root annotation, invalid characters", + }, } for _, test := range tests { diff --git a/internal/telemetry/collector_test.go b/internal/telemetry/collector_test.go index d7fcee84f3..93e6be28a5 100644 --- a/internal/telemetry/collector_test.go +++ b/internal/telemetry/collector_test.go @@ -907,6 +907,36 @@ func TestInvalidStandardIngressAnnotations(t *testing.T) { } } +func TestAppRootAnnotationTelemetry(t *testing.T) { + t.Parallel() + + buf := &bytes.Buffer{} + exp := &telemetry.StdoutExporter{Endpoint: buf} + + annotations := map[string]string{ + "nginx.org/app-root": "/coffee", + } + + configurator := newConfiguratorWithIngressWithCustomAnnotations(t, annotations) + + cfg := telemetry.CollectorConfig{ + Configurator: configurator, + K8sClientReader: newTestClientset(node1, kubeNS), + Version: telemetryNICData.ProjectVersion, + } + + c, err := telemetry.NewCollector(cfg, telemetry.WithExporter(exp)) + if err != nil { + t.Fatal(err) + } + c.Collect(context.Background()) + + got := buf.String() + if !strings.Contains(got, "nginx.org/app-root") { + t.Errorf("expected app-root annotation to be collected in telemetry, got: %v", got) + } +} + func TestIngressCountReportsNumberOfDeployedIngresses(t *testing.T) { t.Parallel() From 7dc08d6619786cdbc31d4ea7d6c972027eb73cf1 Mon Sep 17 00:00:00 2001 From: AlexFenlon Date: Mon, 15 Dec 2025 14:56:27 +0000 Subject: [PATCH 2/5] fix indentation in tmpl files Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: AlexFenlon --- internal/configs/version1/nginx-plus.ingress.tmpl | 10 +++++----- internal/configs/version1/nginx.ingress.tmpl | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl index cef055ac1f..cf8a61b713 100644 --- a/internal/configs/version1/nginx-plus.ingress.tmpl +++ b/internal/configs/version1/nginx-plus.ingress.tmpl @@ -172,11 +172,11 @@ server { {{$value}}{{end}} {{- end}} - {{ if $server.AppRoot }} - if ($uri = /) { - return 302 $scheme://$http_host{{ $server.AppRoot }}; - } - {{ end }} + {{ if $server.AppRoot }} + if ($uri = /) { + return 302 $scheme://$http_host{{ $server.AppRoot }}; + } + {{ end }} {{- range $healthCheck := $server.HealthChecks}} location @hc-{{$healthCheck.UpstreamName}} { diff --git a/internal/configs/version1/nginx.ingress.tmpl b/internal/configs/version1/nginx.ingress.tmpl index 92aa55e221..e36aeb8f89 100644 --- a/internal/configs/version1/nginx.ingress.tmpl +++ b/internal/configs/version1/nginx.ingress.tmpl @@ -120,7 +120,7 @@ server { if ($uri = /) { return 302 $scheme://$http_host{{ $server.AppRoot }}; } - {{ end }} + {{ end }} {{- range $location := $server.Locations}} location {{ makeLocationPath $location $.Ingress.Annotations | printf }} { From b45473421f71933c66c920782736f126e97bdfce Mon Sep 17 00:00:00 2001 From: Alex Fenlon Date: Mon, 15 Dec 2025 15:08:45 +0000 Subject: [PATCH 3/5] fix spacing in tmpl files --- internal/configs/version1/nginx-plus.ingress.tmpl | 4 ++-- internal/configs/version1/nginx.ingress.tmpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl index cf8a61b713..233680317c 100644 --- a/internal/configs/version1/nginx-plus.ingress.tmpl +++ b/internal/configs/version1/nginx-plus.ingress.tmpl @@ -172,11 +172,11 @@ server { {{$value}}{{end}} {{- end}} - {{ if $server.AppRoot }} + {{- if $server.AppRoot }} if ($uri = /) { return 302 $scheme://$http_host{{ $server.AppRoot }}; } - {{ end }} + {{- end }} {{- range $healthCheck := $server.HealthChecks}} location @hc-{{$healthCheck.UpstreamName}} { diff --git a/internal/configs/version1/nginx.ingress.tmpl b/internal/configs/version1/nginx.ingress.tmpl index e36aeb8f89..5c6d02c9bf 100644 --- a/internal/configs/version1/nginx.ingress.tmpl +++ b/internal/configs/version1/nginx.ingress.tmpl @@ -116,11 +116,11 @@ server { {{- range $value := $server.ServerSnippets}} {{$value}}{{- end}} - {{ if $server.AppRoot }} + {{- if $server.AppRoot }} if ($uri = /) { return 302 $scheme://$http_host{{ $server.AppRoot }}; } - {{ end }} + {{- end }} {{- range $location := $server.Locations}} location {{ makeLocationPath $location $.Ingress.Annotations | printf }} { From 6ae2d8c01a3b6235eed0125eb371e8deda2df691 Mon Sep 17 00:00:00 2001 From: Alex Fenlon Date: Fri, 19 Dec 2025 15:54:34 +0000 Subject: [PATCH 4/5] Add path validation and deny app-root from minion --- internal/configs/annotations.go | 7 +- internal/configs/annotations_test.go | 110 +++++++++++++++++++++++++++ internal/k8s/validation.go | 7 +- internal/k8s/validation_test.go | 2 +- 4 files changed, 119 insertions(+), 7 deletions(-) diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index 4e8ea29e61..c2f537c0fe 100644 --- a/internal/configs/annotations.go +++ b/internal/configs/annotations.go @@ -75,6 +75,7 @@ var minionDenylist = map[string]bool{ "nginx.org/server-snippets": true, "nginx.org/ssl-ciphers": true, "nginx.org/ssl-prefer-server-ciphers": true, + "nginx.org/app-root": true, "appprotect.f5.com/app_protect_enable": true, "appprotect.f5.com/app_protect_policy": true, "appprotect.f5.com/app_protect_security_log_enable": true, @@ -504,7 +505,11 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool } if appRoot, exists := ingEx.Ingress.Annotations["nginx.org/app-root"]; exists { - cfgParams.AppRoot = appRoot + if !VerifyPath(appRoot) { + nl.Errorf(l, "Ingress %s/%s: Invalid value nginx.org/app-root: got %q. Must start with '/'", ingEx.Ingress.GetNamespace(), ingEx.Ingress.GetName(), appRoot) + } else { + cfgParams.AppRoot = appRoot + } } if useClusterIP, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, UseClusterIPAnnotation, ingEx.Ingress); exists { diff --git a/internal/configs/annotations_test.go b/internal/configs/annotations_test.go index 1d612f5515..c72409ca90 100644 --- a/internal/configs/annotations_test.go +++ b/internal/configs/annotations_test.go @@ -1011,3 +1011,113 @@ func TestSSLRedirectAnnotations(t *testing.T) { }) } } + +func TestAppRootAnnotation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + annotations map[string]string + expected string + }{ + { + name: "valid app-root - coffee path", + annotations: map[string]string{ + "nginx.org/app-root": "/coffee", + }, + expected: "/coffee", + }, + { + name: "valid app-root - nested path with mocha", + annotations: map[string]string{ + "nginx.org/app-root": "/coffee/mocha", + }, + expected: "/coffee/mocha", + }, + { + name: "valid app-root - tea path", + annotations: map[string]string{ + "nginx.org/app-root": "/tea", + }, + expected: "/tea", + }, + { + name: "valid app-root - nested tea path", + annotations: map[string]string{ + "nginx.org/app-root": "/tea/green-tea", + }, + expected: "/tea/green-tea", + }, + { + name: "valid app-root - cafe path", + annotations: map[string]string{ + "nginx.org/app-root": "/cafe", + }, + expected: "/cafe", + }, + { + name: "invalid app-root - does not start with slash", + annotations: map[string]string{ + "nginx.org/app-root": "coffee", + }, + expected: "", // Should remain empty due to invalid path + }, + { + name: "invalid app-root - contains invalid characters", + annotations: map[string]string{ + "nginx.org/app-root": "/tea$mocha", + }, + expected: "", // Should remain empty due to invalid characters + }, + { + name: "invalid app-root - contains curly braces", + annotations: map[string]string{ + "nginx.org/app-root": "/coffee{test}", + }, + expected: "", // Should remain empty due to invalid characters + }, + { + name: "invalid app-root - contains semicolon", + annotations: map[string]string{ + "nginx.org/app-root": "/tea;chai", + }, + expected: "", // Should remain empty due to invalid characters + }, + { + name: "invalid app-root - contains whitespace", + annotations: map[string]string{ + "nginx.org/app-root": "/tea chai", + }, + expected: "", // Should remain empty due to invalid characters + }, + { + name: "no app-root annotation", + annotations: map[string]string{}, + expected: "", // Should remain empty when annotation is missing + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ingEx := &IngressEx{ + Ingress: &networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ingress", + Namespace: "default", + Annotations: tt.annotations, + }, + }, + } + + baseCfgParams := NewDefaultConfigParams(context.Background(), false) + result := parseAnnotations(ingEx, baseCfgParams, false, false, false, false, false) + + if result.AppRoot != tt.expected { + t.Errorf("Test %q: expected AppRoot %q, got %q", tt.name, tt.expected, result.AppRoot) + } + }) + } +} diff --git a/internal/k8s/validation.go b/internal/k8s/validation.go index b02fa0a4a9..d62a89f7fc 100644 --- a/internal/k8s/validation.go +++ b/internal/k8s/validation.go @@ -394,11 +394,8 @@ func validateAppRootAnnotation(context *annotationValidationContext) field.Error return allErrs } - // Validate that the path doesn't contain invalid characters - // Allow alphanumeric, hyphens, underscores, dots, and forward slashes - validPath := regexp.MustCompile(`^/[a-zA-Z0-9\-_./]*$`) - if !validPath.MatchString(path) { - allErrs = append(allErrs, field.Invalid(context.fieldPath, path, "contains invalid characters, only alphanumeric, hyphens, underscores, dots, and forward slashes are allowed")) + if !configs.VerifyPath(path) { + allErrs = append(allErrs, field.Invalid(context.fieldPath, path, "path must start with '/' and must not include any special character, '{', '}', ';' or '$'")) return allErrs } diff --git a/internal/k8s/validation_test.go b/internal/k8s/validation_test.go index b5c773af74..5452ea7ac6 100644 --- a/internal/k8s/validation_test.go +++ b/internal/k8s/validation_test.go @@ -3541,7 +3541,7 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { appProtectDosEnabled: false, internalRoutesEnabled: false, expectedErrors: []string{ - `annotations.nginx.org/app-root: Invalid value: "/tea$1": contains invalid characters, only alphanumeric, hyphens, underscores, dots, and forward slashes are allowed`, + `annotations.nginx.org/app-root: Invalid value: "/tea$1": path must start with '/' and must not include any special character, '{', '}', ';' or '$'`, }, msg: "invalid nginx.org/app-root annotation, invalid characters", }, From 24bd39aec0bd35411e137648782830251a31c76c Mon Sep 17 00:00:00 2001 From: Alex Fenlon Date: Fri, 19 Dec 2025 17:04:39 +0000 Subject: [PATCH 5/5] Update valiadation for annotation --- internal/configs/annotations.go | 6 +----- internal/k8s/validation.go | 7 ++++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index c2f537c0fe..69d024f0ab 100644 --- a/internal/configs/annotations.go +++ b/internal/configs/annotations.go @@ -505,11 +505,7 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool } if appRoot, exists := ingEx.Ingress.Annotations["nginx.org/app-root"]; exists { - if !VerifyPath(appRoot) { - nl.Errorf(l, "Ingress %s/%s: Invalid value nginx.org/app-root: got %q. Must start with '/'", ingEx.Ingress.GetNamespace(), ingEx.Ingress.GetName(), appRoot) - } else { - cfgParams.AppRoot = appRoot - } + cfgParams.AppRoot = appRoot } if useClusterIP, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, UseClusterIPAnnotation, ingEx.Ingress); exists { diff --git a/internal/k8s/validation.go b/internal/k8s/validation.go index d62a89f7fc..58ee1d699d 100644 --- a/internal/k8s/validation.go +++ b/internal/k8s/validation.go @@ -363,6 +363,7 @@ var ( }, appRootAnnotation: { validateAppRootAnnotation, + validateRewriteTargetAnnotation, }, } annotationNames = sortedAnnotationNames(annotationValidations) @@ -394,9 +395,9 @@ func validateAppRootAnnotation(context *annotationValidationContext) field.Error return allErrs } - if !configs.VerifyPath(path) { - allErrs = append(allErrs, field.Invalid(context.fieldPath, path, "path must start with '/' and must not include any special character, '{', '}', ';' or '$'")) - return allErrs + validPath := regexp.MustCompile(`^/[a-zA-Z0-9\-_./]*$`) + if !validPath.MatchString(path) { + allErrs = append(allErrs, field.Invalid(context.fieldPath, path, "contains invalid characters, only alphanumeric, hyphens, underscores, dots, and forward slashes are allowed")) } // Ensure path doesn't end with /