Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/configs/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions internal/configs/config_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/configs/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions internal/configs/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions internal/configs/version1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions internal/configs/version1/nginx-plus.ingress.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
6 changes: 6 additions & 0 deletions internal/configs/version1/nginx.ingress.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}";
Expand Down
38 changes: 38 additions & 0 deletions internal/k8s/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -360,6 +361,9 @@ var (
useClusterIPAnnotation: {
validateBoolAnnotation,
},
appRootAnnotation: {
validateAppRootAnnotation,
},
}
annotationNames = sortedAnnotationNames(annotationValidations)
)
Expand All @@ -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{}

Expand Down
80 changes: 80 additions & 0 deletions internal/k8s/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3409,6 +3409,86 @@ func TestValidateNginxIngressAnnotations(t *testing.T) {
},
msg: "invalid nginx.org/rewrite-target annotation, does not start with slash",
},
{
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 {
Expand Down
30 changes: 30 additions & 0 deletions internal/telemetry/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Loading