diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index 78b23477c..31f905b95 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -527,6 +527,12 @@ func (n *NGINXController) getBackendServers(ingresses []*ingress.Ingress) ([]*in addLoc := true for _, loc := range server.Locations { if loc.Path == nginxPath { + // Same paths but different types are allowed + // (same type means overlap in the path definition) + if *loc.PathType != *path.PathType { + break + } + addLoc = false if !loc.IsDefBackend { @@ -543,6 +549,7 @@ func (n *NGINXController) getBackendServers(ingresses []*ingress.Ingress) ([]*in loc.Port = ups.Port loc.Service = ups.Service loc.Ingress = ing + locationApplyAnnotations(loc, anns) if loc.Redirect.FromToWWW { @@ -556,9 +563,9 @@ func (n *NGINXController) getBackendServers(ingresses []*ingress.Ingress) ([]*in if addLoc { klog.V(3).Infof("Adding location %q for server %q with upstream %q (Ingress %q)", nginxPath, server.Hostname, ups.Name, ingKey) - loc := &ingress.Location{ Path: nginxPath, + PathType: path.PathType, Backend: ups.Name, IsDefBackend: false, Service: ups.Service, @@ -953,12 +960,14 @@ func (n *NGINXController) createServers(data []*ingress.Ingress, } // initialize default server and root location + pathTypePrefix := networking.PathTypePrefix servers[defServerName] = &ingress.Server{ Hostname: defServerName, SSLCert: n.getDefaultSSLCertificate(), Locations: []*ingress.Location{ { Path: rootLocation, + PathType: &pathTypePrefix, IsDefBackend: true, Backend: du.Name, Proxy: ngxProxy, @@ -1025,8 +1034,10 @@ func (n *NGINXController) createServers(data []*ingress.Ingress, continue } + pathTypePrefix := networking.PathTypePrefix loc := &ingress.Location{ Path: rootLocation, + PathType: &pathTypePrefix, IsDefBackend: true, Backend: un, Service: &apiv1.Service{}, @@ -1311,7 +1322,7 @@ func mergeAlternativeBackends(ing *ingress.Ingress, upstreams map[string]*ingres break } - if canMergeBackend(priUps, altUps) && loc.Path == path.Path { + if canMergeBackend(priUps, altUps) && loc.Path == path.Path && *loc.PathType == *path.PathType { klog.V(2).Infof("matching backend %v found for alternative backend %v", priUps.Name, altUps.Name) diff --git a/internal/ingress/controller/controller_test.go b/internal/ingress/controller/controller_test.go index 356c088a2..a812d56eb 100644 --- a/internal/ingress/controller/controller_test.go +++ b/internal/ingress/controller/controller_test.go @@ -257,6 +257,8 @@ func TestCheckIngress(t *testing.T) { }) } +var pathPrefix = networking.PathTypePrefix + func TestMergeAlternativeBackends(t *testing.T) { testCases := map[string]struct { ingress *ingress.Ingress @@ -279,7 +281,8 @@ func TestMergeAlternativeBackends(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/", + Path: "/", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-canary", ServicePort: intstr.IntOrString{ @@ -314,8 +317,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "example.com", Locations: []*ingress.Location{ { - Path: "/", - Backend: "example-http-svc-80", + Path: "/", + PathType: &pathPrefix, + Backend: "example-http-svc-80", }, }, }, @@ -339,8 +343,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "example.com", Locations: []*ingress.Location{ { - Path: "/", - Backend: "example-http-svc-80", + Path: "/", + PathType: &pathPrefix, + Backend: "example-http-svc-80", }, }, }, @@ -360,7 +365,8 @@ func TestMergeAlternativeBackends(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/", + Path: "/", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "foo-http-svc-canary", ServicePort: intstr.IntOrString{ @@ -379,7 +385,8 @@ func TestMergeAlternativeBackends(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/", + Path: "/", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-canary", ServicePort: intstr.IntOrString{ @@ -426,8 +433,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "foo.bar", Locations: []*ingress.Location{ { - Path: "/", - Backend: "example-foo-http-svc-80", + Path: "/", + PathType: &pathPrefix, + Backend: "example-foo-http-svc-80", }, }, }, @@ -435,8 +443,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "example.com", Locations: []*ingress.Location{ { - Path: "/", - Backend: "example-http-svc-80", + Path: "/", + PathType: &pathPrefix, + Backend: "example-http-svc-80", }, }, }, @@ -472,8 +481,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "example.com", Locations: []*ingress.Location{ { - Path: "/", - Backend: "example-http-svc-80", + Path: "/", + PathType: &pathPrefix, + Backend: "example-http-svc-80", }, }, }, @@ -493,7 +503,8 @@ func TestMergeAlternativeBackends(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/", + Path: "/", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-canary", ServicePort: intstr.IntOrString{ @@ -557,8 +568,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "_", Locations: []*ingress.Location{ { - Path: "/", - Backend: "example-http-svc-80", + Path: "/", + PathType: &pathPrefix, + Backend: "example-http-svc-80", }, }, }, @@ -582,8 +594,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "_", Locations: []*ingress.Location{ { - Path: "/", - Backend: "example-http-svc-80", + Path: "/", + PathType: &pathPrefix, + Backend: "example-http-svc-80", }, }, }, @@ -623,8 +636,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "_", Locations: []*ingress.Location{ { - Path: "/", - Backend: "upstream-default-backend", + Path: "/", + PathType: &pathPrefix, + Backend: "upstream-default-backend", }, }, }, @@ -635,8 +649,9 @@ func TestMergeAlternativeBackends(t *testing.T) { Hostname: "_", Locations: []*ingress.Location{ { - Path: "/", - Backend: "upstream-default-backend", + Path: "/", + PathType: &pathPrefix, + Backend: "upstream-default-backend", }, }, }, @@ -943,7 +958,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/", + Path: "/", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-canary", ServicePort: intstr.IntOrString{ @@ -1002,7 +1018,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/", + Path: "/", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc", ServicePort: intstr.IntOrString{ @@ -1038,7 +1055,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/", + Path: "/", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-canary", ServicePort: intstr.IntOrString{ @@ -1106,7 +1124,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/a", + Path: "/a", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-1", ServicePort: intstr.IntOrString{ @@ -1142,7 +1161,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/a", + Path: "/a", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-2", ServicePort: intstr.IntOrString{ @@ -1178,7 +1198,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/b", + Path: "/b", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-2", ServicePort: intstr.IntOrString{ @@ -1214,7 +1235,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/b", + Path: "/b", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-1", ServicePort: intstr.IntOrString{ @@ -1250,7 +1272,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/c", + Path: "/c", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-1", ServicePort: intstr.IntOrString{ @@ -1286,7 +1309,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/c", + Path: "/c", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "http-svc-2", ServicePort: intstr.IntOrString{ @@ -1370,7 +1394,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/path1", + Path: "/path1", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "path1-svc", ServicePort: intstr.IntOrString{ @@ -1409,7 +1434,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/path2", + Path: "/path2", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "path2-svc", ServicePort: intstr.IntOrString{ @@ -1473,7 +1499,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/path1", + Path: "/path1", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "path1-svc", ServicePort: intstr.IntOrString{ @@ -1512,7 +1539,8 @@ func TestGetBackendServers(t *testing.T) { HTTP: &networking.HTTPIngressRuleValue{ Paths: []networking.HTTPIngressPath{ { - Path: "/path2", + Path: "/path2", + PathType: &pathPrefix, Backend: networking.IngressBackend{ ServiceName: "path2-svc", ServicePort: intstr.IntOrString{ diff --git a/internal/ingress/controller/store/store.go b/internal/ingress/controller/store/store.go index 1cb6c4d5f..976cbbd8d 100644 --- a/internal/ingress/controller/store/store.go +++ b/internal/ingress/controller/store/store.go @@ -26,8 +26,6 @@ import ( "sync" "time" - "k8s.io/klog" - "github.com/eapache/channels" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" @@ -36,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/labels" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/informers" clientset "k8s.io/client-go/kubernetes" @@ -43,6 +42,7 @@ import ( clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" + "k8s.io/klog" "k8s.io/ingress-nginx/internal/file" "k8s.io/ingress-nginx/internal/ingress" @@ -639,6 +639,9 @@ func isCatchAllIngress(spec networkingv1beta1.IngressSpec) bool { return spec.Backend != nil && len(spec.Rules) == 0 } +// Default path type is Prefix to not break existing definitions +var defaultPathType = networkingv1beta1.PathTypePrefix + // syncIngress parses ingress annotations converting the value of the // annotation to a go struct func (s *k8sStore) syncIngress(ing *networkingv1beta1.Ingress) { @@ -659,6 +662,17 @@ func (s *k8sStore) syncIngress(ing *networkingv1beta1.Ingress) { if path.Path == "" { copyIng.Spec.Rules[ri].HTTP.Paths[pi].Path = "/" } + + if path.PathType == nil { + copyIng.Spec.Rules[ri].HTTP.Paths[pi].PathType = &defaultPathType + continue + } + + // PathType ImplementationSpecific is not supported. + // Set type to PathTypePrefix. + if *path.PathType == networkingv1beta1.PathTypeImplementationSpecific { + copyIng.Spec.Rules[ri].HTTP.Paths[pi].PathType = &defaultPathType + } } } @@ -925,8 +939,8 @@ func (s k8sStore) GetRunningControllerPodsCount() int { var runtimeScheme = k8sruntime.NewScheme() func init() { - extensionsv1beta1.AddToScheme(runtimeScheme) - networkingv1beta1.AddToScheme(runtimeScheme) + utilruntime.Must(extensionsv1beta1.AddToScheme(runtimeScheme)) + utilruntime.Must(networkingv1beta1.AddToScheme(runtimeScheme)) } func fromExtensions(old *extensionsv1beta1.Ingress) (*networkingv1beta1.Ingress, error) { @@ -961,9 +975,6 @@ func toIngress(obj interface{}) (*networkingv1beta1.Ingress, bool) { return nil, false } -// Default path type is Prefix to not break existing definitions -var defaultPathType = networkingv1beta1.PathTypePrefix - func setDefaultPathTypeIfEmpty(ing *networkingv1beta1.Ingress) { for _, rule := range ing.Spec.Rules { if rule.IngressRuleValue.HTTP == nil { diff --git a/internal/ingress/controller/template/template.go b/internal/ingress/controller/template/template.go index d72666ed9..9d22b5257 100644 --- a/internal/ingress/controller/template/template.go +++ b/internal/ingress/controller/template/template.go @@ -39,6 +39,7 @@ import ( "github.com/pkg/errors" + networkingv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog" @@ -402,6 +403,11 @@ func buildLocation(input interface{}, enforceRegex bool) string { if enforceRegex { return fmt.Sprintf(`~* "^%s"`, path) } + + if location.PathType != nil && *location.PathType == networkingv1beta1.PathTypeExact { + return fmt.Sprintf(`= %s`, path) + } + return path } diff --git a/internal/ingress/types.go b/internal/ingress/types.go index b58dee3d7..dcf56a015 100644 --- a/internal/ingress/types.go +++ b/internal/ingress/types.go @@ -224,6 +224,8 @@ type Location struct { // a '/'. If unspecified, the path defaults to a catch all sending // traffic to the backend. Path string `json:"path"` + // PathType represents the type of path referred to by a HTTPIngressPath. + PathType *networking.PathType `json:"pathType"` // IsDefBackend indicates if service specified in the Ingress // contains active endpoints or not. Returning true means the location // uses the default backend. diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index 3d6904b94..dc1c81a81 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -25,6 +25,7 @@ import ( "k8s.io/component-base/logs" // required + _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/ingress-nginx/test/e2e/framework" @@ -34,6 +35,7 @@ import ( _ "k8s.io/ingress-nginx/test/e2e/dbg" _ "k8s.io/ingress-nginx/test/e2e/defaultbackend" _ "k8s.io/ingress-nginx/test/e2e/gracefulshutdown" + _ "k8s.io/ingress-nginx/test/e2e/ingress" _ "k8s.io/ingress-nginx/test/e2e/leaks" _ "k8s.io/ingress-nginx/test/e2e/loadbalance" _ "k8s.io/ingress-nginx/test/e2e/lua" diff --git a/test/e2e/framework/util.go b/test/e2e/framework/util.go index b3fe1bf8e..e14010509 100644 --- a/test/e2e/framework/util.go +++ b/test/e2e/framework/util.go @@ -61,13 +61,6 @@ func Failf(format string, args ...interface{}) { ginkgo.Fail(nowStamp()+": "+msg, 1) } -// Skipf logs to the INFO logs and skips the test. -func Skipf(format string, args ...interface{}) { - msg := fmt.Sprintf(format, args...) - log("INFO", msg) - ginkgo.Skip(nowStamp() + ": " + msg) -} - // RestclientConfig deserializes the contents of a kubeconfig file into a Config object. func RestclientConfig(config, context string) (*api.Config, error) { Logf(">>> config: %s\n", config) diff --git a/test/e2e/ingress/pathtype_exact.go b/test/e2e/ingress/pathtype_exact.go new file mode 100644 index 000000000..7f6820537 --- /dev/null +++ b/test/e2e/ingress/pathtype_exact.go @@ -0,0 +1,117 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ingress + +import ( + "net/http" + "strings" + + "github.com/onsi/ginkgo" + "github.com/stretchr/testify/assert" + networkingv1beta1 "k8s.io/api/networking/v1beta1" + + "k8s.io/ingress-nginx/test/e2e/framework" +) + +var _ = framework.IngressNginxDescribe("[Ingress] [PathType] exact", func() { + f := framework.NewDefaultFramework("exact") + + ginkgo.BeforeEach(func() { + f.NewEchoDeployment() + }) + + ginkgo.It("should choose exact location for /exact", func() { + if !f.IsIngressV1Ready { + ginkgo.Skip("Test requires Kubernetes v1.18 or higher") + } + + host := "exact.path" + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/configuration-snippet": `more_set_input_headers "pathType: exact";`, + } + + var exactPathType = networkingv1beta1.PathTypeExact + ing := framework.NewSingleIngress("exact", "/exact", host, f.Namespace, framework.EchoService, 80, annotations) + ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &exactPathType + f.EnsureIngress(ing) + + annotations = map[string]string{ + "nginx.ingress.kubernetes.io/configuration-snippet": `more_set_input_headers "pathType: prefix";`, + } + + ing = framework.NewSingleIngress("exact-sufix", "/exact", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, host) && + strings.Contains(server, "location = /exact") && + strings.Contains(server, "location /exact") + }) + + body := f.HTTPTestClient(). + GET("/exact"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK). + Body(). + Raw() + + assert.NotContains(ginkgo.GinkgoT(), body, "pathtype=prefix") + assert.Contains(ginkgo.GinkgoT(), body, "pathtype=exact") + + body = f.HTTPTestClient(). + GET("/exact/sufix"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK). + Body(). + Raw() + + assert.Contains(ginkgo.GinkgoT(), body, "pathtype=prefix") + + annotations = map[string]string{ + "nginx.ingress.kubernetes.io/configuration-snippet": ` + more_set_input_headers "pathType: prefix"; + more_set_input_headers "duplicated: true"; + `, + } + + ing = framework.NewSingleIngress("duplicated-prefix", "/exact", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, host) && + strings.Contains(server, "location = /exact") && + strings.Contains(server, "location /exact") + }) + + body = f.HTTPTestClient(). + GET("/exact/sufix"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK). + Body(). + Raw() + + assert.Contains(ginkgo.GinkgoT(), body, "pathtype=prefix") + assert.NotContains(ginkgo.GinkgoT(), body, "pathtype=exact") + assert.NotContains(ginkgo.GinkgoT(), body, "duplicated=true") + }) +})