diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 6ae1b1fea..dd9163f4b 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -127,6 +127,7 @@ The following general annotations are applicable on the Ingress object: | `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. | | `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Override the default frontend rule type. Default: `PathPrefix`. | | `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access. all source IPs are permitted if the list is empty or a single range is ill-formatted. | +| `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. Non-root paths will not be affected by this annotation and handled normally. This annotation may not be combined with the `ReplacePath` rule type or any other annotation leveraging that rule type. Trying to do so leads to an error and the corresponding Ingress object being ignored. | <1> `traefik.ingress.kubernetes.io/error-pages` example: diff --git a/provider/kubernetes/annotations.go b/provider/kubernetes/annotations.go index dddc18823..648b78d7c 100644 --- a/provider/kubernetes/annotations.go +++ b/provider/kubernetes/annotations.go @@ -29,6 +29,7 @@ const ( annotationKubernetesRateLimit = "ingress.kubernetes.io/rate-limit" annotationKubernetesErrorPages = "ingress.kubernetes.io/error-pages" annotationKubernetesBuffering = "ingress.kubernetes.io/buffering" + annotationKubernetesAppRoot = "ingress.kubernetes.io/app-root" annotationKubernetesSSLRedirect = "ingress.kubernetes.io/ssl-redirect" annotationKubernetesHSTSMaxAge = "ingress.kubernetes.io/hsts-max-age" diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index e3f4ee8c2..66ac82e28 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -226,7 +226,13 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } } - if rule := getRuleForPath(pa, i); rule != "" { + rule, err := getRuleForPath(pa, i) + if err != nil { + log.Errorf("Failed to get rule for ingress %s/%s: %s", i.Namespace, i.Name, err) + delete(templateObjects.Frontends, baseName) + continue + } + if rule != "" { templateObjects.Frontends[baseName].Routes[pa.Path] = types.Route{ Rule: rule, } @@ -313,19 +319,34 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config return configuration } -func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) string { +func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) (string, error) { if len(pa.Path) == 0 { - return "" + return "", nil } ruleType := getStringValue(i.Annotations, annotationKubernetesRuleType, ruleTypePathPrefix) rules := []string{ruleType + ":" + pa.Path} - if rewriteTarget := getStringValue(i.Annotations, annotationKubernetesRewriteTarget, ""); rewriteTarget != "" { - rules = append(rules, ruleTypeReplacePath+":"+rewriteTarget) + var pathReplaceAnnotation string + if ruleType == ruleTypeReplacePath { + pathReplaceAnnotation = annotationKubernetesRuleType } - return strings.Join(rules, ";") + if rewriteTarget := getStringValue(i.Annotations, annotationKubernetesRewriteTarget, ""); rewriteTarget != "" { + if pathReplaceAnnotation != "" { + return "", fmt.Errorf("rewrite-target must not be used together with annotation %q", pathReplaceAnnotation) + } + rules = append(rules, ruleTypeReplacePath+":"+rewriteTarget) + pathReplaceAnnotation = annotationKubernetesRewriteTarget + } + + if rootPath := label.GetStringValue(i.Annotations, annotationKubernetesAppRoot, ""); rootPath != "" && pa.Path == "/" { + if pathReplaceAnnotation != "" { + return "", fmt.Errorf("app-root must not be used together with annotation %q", pathReplaceAnnotation) + } + rules = append(rules, ruleTypeReplacePath+":"+rootPath) + } + return strings.Join(rules, ";"), nil } func getRuleForHost(host string) string { diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index c33ebe585..8bb63791e 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -737,6 +737,45 @@ rateset: iPaths(onePath(iPath("/ratelimit"), iBackend("service1", intstr.FromInt(80))))), ), ), + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesAppRoot, "/root"), + iRules( + iRule( + iHost("root"), + iPaths( + onePath(iPath("/"), iBackend("service1", intstr.FromInt(80))), + onePath(iPath("/root1"), iBackend("service1", intstr.FromInt(80))), + ), + ), + ), + ), + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesAppRoot, "/root2"), + iAnnotation(annotationKubernetesRewriteTarget, "/abc"), + iRules( + iRule( + iHost("root2"), + iPaths( + onePath(iPath("/"), iBackend("service2", intstr.FromInt(80))), + ), + ), + ), + ), + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesRuleType, ruleTypeReplacePath), + iAnnotation(annotationKubernetesRewriteTarget, "/abc"), + iRules( + iRule( + iHost("root2"), + iPaths( + onePath(iPath("/"), iBackend("service2", intstr.FromInt(80))), + ), + ), + ), + ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, "traefik"), @@ -880,6 +919,20 @@ rateset: server("http://example.com", weight(1))), lbMethod("wrr"), ), + backend("root/", + servers( + server("http://example.com", weight(1))), + lbMethod("wrr"), + ), + backend("root/root1", + servers( + server("http://example.com", weight(1))), + lbMethod("wrr"), + ), + backend("root2/", + servers(), + lbMethod("wrr"), + ), ), frontends( frontend("foo/bar", @@ -994,6 +1047,20 @@ rateset: route("/customheaders", "PathPrefix:/customheaders"), route("custom-headers", "Host:custom-headers")), ), + frontend("root/", + passHostHeader(), + routes( + route("/", "PathPrefix:/;ReplacePath:/root"), + route("root", "Host:root"), + ), + ), + frontend("root/root1", + passHostHeader(), + routes( + route("/root1", "PathPrefix:/root1"), + route("root", "Host:root"), + ), + ), ), )