Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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