diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index 2a269863a..5a0a8398d 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -6,7 +6,11 @@ The Kubernetes Ingress Controller. The Traefik Kubernetes Ingress provider is a Kubernetes Ingress controller; that is to say, it manages access to a cluster services by supporting the [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) specification. -## Enabling and using the provider +## Routing Configuration + +See the dedicated section in [routing](../routing/providers/kubernetes-ingress.md). + +## Enabling and Using the Provider As usual, the provider is enabled through the static configuration: @@ -23,43 +27,9 @@ providers: --providers.kubernetesingress=true ``` -The provider then watches for incoming ingresses events, such as the example below, and derives the corresponding dynamic configuration from it, which in turn will create the resulting routers, services, handlers, etc. - -```yaml tab="File (YAML)" -kind: Ingress -apiVersion: extensions/v1beta1 -metadata: - name: "foo" - namespace: production - -spec: - rules: - - host: foo.com - http: - paths: - - path: /bar - backend: - serviceName: service1 - servicePort: 80 - - path: /foo - backend: - serviceName: service1 - servicePort: 80 -``` - -## LetsEncrypt Support with the Ingress Provider - -By design, Traefik is a stateless application, meaning that it only derives its configuration from the environment it runs in, without additional configuration. -For this reason, users can run multiple instances of Traefik at the same time to achieve HA, as is a common pattern in the kubernetes ecosystem. - -When using a single instance of Traefik with LetsEncrypt, no issues should be encountered, however this could be a single point of failure. -Unfortunately, it is not possible to run multiple instances of Traefik 2.0 with LetsEncrypt enabled, because there is no way to ensure that the correct instance of Traefik will receive the challenge request, and subsequent responses. -Previous versions of Traefik used a [KV store](https://docs.traefik.io/v1.7/configuration/acme/#storage) to attempt to achieve this, but due to sub-optimal performance was dropped as a feature in 2.0. - -If you require LetsEncrypt with HA in a kubernetes environment, we recommend using [TraefikEE](https://containo.us/traefikee/) where distributed LetsEncrypt is a supported feature. - -If you are wanting to continue to run Traefik Community Edition, LetsEncrypt HA can be achieved by using a Certificate Controller such as [Cert-Manager](https://docs.cert-manager.io/en/latest/index.html). -When using Cert-Manager to manage certificates, it will create secrets in your namespaces that can be referenced as TLS secrets in your [ingress objects](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls). +The provider then watches for incoming ingresses events, such as the example below, +and derives the corresponding dynamic configuration from it, +which in turn will create the resulting routers, services, handlers, etc. ## Provider Configuration @@ -337,6 +307,20 @@ providers: --providers.kubernetesingress.throttleDuration=10s ``` -## Further +### Further If one wants to know more about the various aspects of the Ingress spec that Traefik supports, many examples of Ingresses definitions are located in the tests [data](https://github.com/containous/traefik/tree/v2.0/pkg/provider/kubernetes/ingress/fixtures) of the Traefik repository. + +## LetsEncrypt Support with the Ingress Provider + +By design, Traefik is a stateless application, meaning that it only derives its configuration from the environment it runs in, without additional configuration. +For this reason, users can run multiple instances of Traefik at the same time to achieve HA, as is a common pattern in the kubernetes ecosystem. + +When using a single instance of Traefik with LetsEncrypt, no issues should be encountered, however this could be a single point of failure. +Unfortunately, it is not possible to run multiple instances of Traefik 2.0 with LetsEncrypt enabled, because there is no way to ensure that the correct instance of Traefik will receive the challenge request, and subsequent responses. +Previous versions of Traefik used a [KV store](https://docs.traefik.io/v1.7/configuration/acme/#storage) to attempt to achieve this, but due to sub-optimal performance was dropped as a feature in 2.0. + +If you require LetsEncrypt with HA in a kubernetes environment, we recommend using [TraefikEE](https://containo.us/traefikee/) where distributed LetsEncrypt is a supported feature. + +If you are wanting to continue to run Traefik Community Edition, LetsEncrypt HA can be achieved by using a Certificate Controller such as [Cert-Manager](https://docs.cert-manager.io/en/latest/index.html). +When using Cert-Manager to manage certificates, it will create secrets in your namespaces that can be referenced as TLS secrets in your [ingress objects](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls). diff --git a/docs/content/routing/providers/kubernetes-ingress.md b/docs/content/routing/providers/kubernetes-ingress.md new file mode 100644 index 000000000..50669f645 --- /dev/null +++ b/docs/content/routing/providers/kubernetes-ingress.md @@ -0,0 +1,298 @@ +# Traefik & Kubernetes + +The Kubernetes Ingress Controller. +{: .subtitle } + +## Routing Configuration + +The provider then watches for incoming ingresses events, such as the example below, +and derives the corresponding dynamic configuration from it, +which in turn will create the resulting routers, services, handlers, etc. + +```yaml +kind: Ingress +apiVersion: extensions/v1beta1 +metadata: + name: foo + namespace: production + +spec: + rules: + - host: foo.com + http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 + - path: /foo + backend: + serviceName: service1 + servicePort: 80 + + tls: + - secretName: mySecret +``` + +### Annotations + +??? example + + ```yaml tab="Ingress" + kind: Ingress + apiVersion: extensions/v1beta1 + metadata: + name: foo + namespace: production + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + + spec: + rules: + - host: foo.com + http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 + - path: /foo + backend: + serviceName: service1 + servicePort: 80 + ``` + + ```yaml tab="Service" + kind: Service + apiVersion: v1 + metadata: + name: service1 + namespace: testing + annotations: + traefik.ingress.kubernetes.io/service.passhostheader: "false" + + spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 + ``` + +#### On Ingress + +??? info "`traefik.ingress.kubernetes.io/router.entrypoints`" + + See [entry points](../routers/index.md#entrypoints) for more information. + + ```yaml + traefik.ingress.kubernetes.io/router.entrypoints: ep1,ep2 + ``` + +??? info "`traefik.ingress.kubernetes.io/router.middlewares`" + + See [middlewares](../routers/index.md#middlewares) and [middlewares overview](../../middlewares/overview.md) for more information. + + ```yaml + traefik.ingress.kubernetes.io/router.middlewares: auth@file,prefix@kuberntes-crd,cb@file + ``` + +??? info "`traefik.ingress.kubernetes.io/router.priority`" + + See [priority](../routers/index.md#priority) for more information. + + ```yaml + traefik.ingress.kubernetes.io/router.priority: "42" + ``` + +??? info "`traefik.ingress.kubernetes.io/router.pathmatcher`" + + Overrides the default router rule type used for a path. + Only path-related matcher name can be specified: `Path`, `PathPrefix`. + + Default `PathPrefix` + + ```yaml + traefik.ingress.kubernetes.io/router.pathmatcher: Path + ``` + +??? info "`traefik.ingress.kubernetes.io/router.tls`" + + See [tls](../routers/index.md#tls) for more information. + + ```yaml + traefik.ingress.kubernetes.io/router.tls: "true" + ``` + +??? info "`traefik.ingress.kubernetes.io/router.tls.certresolver`" + + See [certResolver](../routers/index.md#certresolver) for more information. + + ```yaml + traefik.ingress.kubernetes.io/router.tls.certresolver: myresolver + ``` + +??? info "`traefik.ingress.kubernetes.io/router.tls.domains.n.main`" + + See [domains](../routers/index.md#domains) for more information. + + ```yaml + traefik.ingress.kubernetes.io/router.tls.domains.0.main: foobar.com + ``` + +??? info "`traefik.ingress.kubernetes.io/router.tls.domains.n.sans`" + + See [domains](../routers/index.md#domains) for more information. + + ```yaml + traefik.ingress.kubernetes.io/router.tls.domains.0.sans: test.foobar.com,dev.foobar.com + ``` + +??? info "`traefik.ingress.kubernetes.io/router.tls.options`" + + See [options](../routers/index.md#options) for more information. + + ```yaml + traefik.ingress.kubernetes.io/router.tls.options: foobar + ``` + +#### On Service + +??? info "`traefik.ingress.kubernetes.io/service.serversscheme`" + + Overrides the default scheme. + + ```yaml + traefik.ingress.kubernetes.io/service.serversscheme: h2c + ``` + +??? info "`traefik.ingress.kubernetes.io/service.passhostheader`" + + See [pass Host header](../services/index.md#pass-host-header) for more information. + + ```yaml + traefik.ingress.kubernetes.io/service.passhostheader: "true" + ``` + +??? info "`traefik.ingress.kubernetes.io/service.sticky`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.ingress.kubernetes.io/service.sticky: "true" + ``` + +??? info "`traefik.ingress.kubernetes.io/service.sticky.cookie.httponly`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true" + ``` + +??? info "`traefik.ingress.kubernetes.io/service.sticky.cookie.name`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.ingress.kubernetes.io/service.sticky.cookie.name: foobar + ``` + +??? info "`traefik.ingress.kubernetes.io/service.sticky.cookie.secure`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true" + ``` + +### TLS + +#### Communication Between Traefik and Pods + +Traefik automatically requests endpoint information based on the service provided in the ingress spec. +Although Traefik will connect directly to the endpoints (pods), +it still checks the service port to see if TLS communication is required. + +There are 3 ways to configure Traefik to use https to communicate with pods: + +1. If the service port defined in the ingress spec is `443` (note that you can still use `targetPort` to use a different port on your pod). +1. If the service port defined in the ingress spec has a name that starts with https (such as `https-api`, `https-web` or just `https`). +1. If the ingress spec includes the annotation `traefik.ingress.kubernetes.io/service.serversscheme: https`. + +If either of those configuration options exist, then the backend communication protocol is assumed to be TLS, +and will connect via TLS automatically. + +!!! info + + Please note that by enabling TLS communication between traefik and your pods, + you will have to have trusted certificates that have the proper trust chain and IP subject name. + If this is not an option, you may need to skip TLS certificate verification. + See the [insecureSkipVerify](../../routing/overview.md#insecureskipverify) setting for more details. + +#### Certificates Management + +??? example "Using a secret" + + ```yaml tab="Ingress" + kind: Ingress + apiVersion: extensions/v1beta1 + metadata: + name: foo + namespace: production + + spec: + rules: + - host: foo.com + http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 + + tls: + - secretName: supersecret + ``` + + ```yaml tab="Secret" + apiVersion: v1 + kind: Secret + metadata: + name: supersecret + + data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + ``` + +TLS certificates can be managed in Secrets objects. + +!!! info + + Only TLS certificates provided by users can be stored in Kubernetes Secrets. + [Let's Encrypt](../../https/acme.md) certificates cannot be managed in Kubernetes Secrets yet. + +## Global Default Backend Ingresses + +Ingresses can be created that look like the following: + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: cheese + +spec: + backend: + serviceName: stilton + servicePort: 80 +``` + +This ingress follows the Global Default Backend property of ingresses. +This will allow users to create a "default router" that will match all unmatched requests. + +!!! info + + Due to Traefik's use of priorities, you may have to set this ingress priority lower than other ingresses in your environment, + to avoid this global ingress from satisfying requests that could match other ingresses. + + To do this, use the `traefik.ingress.kubernetes.io/router.priority` annotation (as seen in [Annotations on Ingress](#on-ingress)) on your ingresses accordingly. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index cebbc5418..0e2a9ceff 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -96,6 +96,7 @@ nav: - 'Providers': - 'Docker': 'routing/providers/docker.md' - 'Kubernetes IngressRoute': 'routing/providers/kubernetes-crd.md' + - 'Kubernetes Ingress': 'routing/providers/kubernetes-ingress.md' - 'Consul Catalog': 'routing/providers/consul-catalog.md' - 'Marathon': 'routing/providers/marathon.md' - 'Rancher': 'routing/providers/rancher.md' diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index 45518f455..821b3ea2d 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -28,19 +28,10 @@ "traefik" ] }, - "whoami-test-https-whoami-tls@kubernetes": { - "service": "default-whoami-http", - "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", - "tls": {}, - "status": "enabled", - "using": [ - "traefik", - "web" - ] - }, "whoami-test-https-whoami@kubernetes": { "service": "default-whoami-http", "rule": "Host(`whoami.test.https`) \u0026\u0026 PathPrefix(`/whoami`)", + "tls": {}, "status": "enabled", "using": [ "traefik", @@ -109,7 +100,6 @@ }, "status": "enabled", "usedBy": [ - "whoami-test-https-whoami-tls@kubernetes", "whoami-test-https-whoami@kubernetes", "whoami-test-whoami@kubernetes" ], diff --git a/pkg/provider/kubernetes/ingress/annotations.go b/pkg/provider/kubernetes/ingress/annotations.go new file mode 100644 index 000000000..3f510b926 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/annotations.go @@ -0,0 +1,108 @@ +package ingress + +import ( + "regexp" + "strings" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/config/label" +) + +const ( + // https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set + annotationsPrefix = "traefik.ingress.kubernetes.io/" +) + +// RouterConfig is the router's root configuration from annotations. +type RouterConfig struct { + Router *RouterIng `json:"router,omitempty"` +} + +// RouterIng is the router's configuration from annotations. +type RouterIng struct { + PathMatcher string `json:"pathMatcher,omitempty"` + EntryPoints []string `json:"entryPoints,omitempty"` + Middlewares []string `json:"middlewares,omitempty"` + Priority int `json:"priority,omitempty"` + TLS *dynamic.RouterTLSConfig `json:"tls,omitempty" label:"allowEmpty"` +} + +// SetDefaults sets the default values. +func (r *RouterIng) SetDefaults() { + r.PathMatcher = defaultPathMatcher +} + +// ServiceConfig is the service's root configuration from annotations. +type ServiceConfig struct { + Service *ServiceIng `json:"service,omitempty"` +} + +// ServiceIng is the service's configuration from annotations. +type ServiceIng struct { + ServersScheme string `json:"serversScheme,omitempty"` + PassHostHeader *bool `json:"passHostHeader"` + Sticky *dynamic.Sticky `json:"sticky,omitempty" label:"allowEmpty"` +} + +// SetDefaults sets the default values. +func (s *ServiceIng) SetDefaults() { + s.PassHostHeader = func(v bool) *bool { return &v }(true) +} + +func parseRouterConfig(annotations map[string]string) (*RouterConfig, error) { + labels := convertAnnotations(annotations) + if len(labels) == 0 { + return nil, nil + } + + cfg := &RouterConfig{} + + err := label.Decode(labels, cfg, "traefik.router.") + if err != nil { + return nil, err + } + + return cfg, nil +} + +func parseServiceConfig(annotations map[string]string) (*ServiceConfig, error) { + labels := convertAnnotations(annotations) + if len(labels) == 0 { + return nil, nil + } + + cfg := &ServiceConfig{} + + err := label.Decode(labels, cfg, "traefik.service.") + if err != nil { + return nil, err + } + + return cfg, nil +} + +func convertAnnotations(annotations map[string]string) map[string]string { + if len(annotations) == 0 { + return nil + } + + exp := regexp.MustCompile(`(.+)\.(\w+)\.(\d+)\.(.+)`) + + result := make(map[string]string) + + for key, value := range annotations { + if !strings.HasPrefix(key, annotationsPrefix) { + continue + } + + newKey := strings.ReplaceAll(key, "ingress.kubernetes.io/", "") + + if exp.MatchString(newKey) { + newKey = exp.ReplaceAllString(newKey, "$1.$2[$3].$4") + } + + result[newKey] = value + } + + return result +} diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go new file mode 100644 index 000000000..751192d2d --- /dev/null +++ b/pkg/provider/kubernetes/ingress/annotations_test.go @@ -0,0 +1,243 @@ +package ingress + +import ( + "testing" + + "github.com/containous/traefik/v2/pkg/config/dynamic" + "github.com/containous/traefik/v2/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_parseRouterConfig(t *testing.T) { + testCases := []struct { + desc string + annotations map[string]string + expected *RouterConfig + }{ + { + desc: "router annotations", + annotations: map[string]string{ + "ingress.kubernetes.io/foo": "bar", + "traefik.ingress.kubernetes.io/foo": "bar", + "traefik.ingress.kubernetes.io/router.pathmatcher": "foobar", + "traefik.ingress.kubernetes.io/router.entrypoints": "foobar,foobar", + "traefik.ingress.kubernetes.io/router.middlewares": "foobar,foobar", + "traefik.ingress.kubernetes.io/router.priority": "42", + "traefik.ingress.kubernetes.io/router.tls": "true", + "traefik.ingress.kubernetes.io/router.tls.certresolver": "foobar", + "traefik.ingress.kubernetes.io/router.tls.domains.0.main": "foobar", + "traefik.ingress.kubernetes.io/router.tls.domains.0.sans": "foobar,foobar", + "traefik.ingress.kubernetes.io/router.tls.domains.1.main": "foobar", + "traefik.ingress.kubernetes.io/router.tls.domains.1.sans": "foobar,foobar", + "traefik.ingress.kubernetes.io/router.tls.options": "foobar", + }, + expected: &RouterConfig{ + Router: &RouterIng{ + PathMatcher: "foobar", + EntryPoints: []string{"foobar", "foobar"}, + Middlewares: []string{"foobar", "foobar"}, + Priority: 42, + TLS: &dynamic.RouterTLSConfig{ + CertResolver: "foobar", + Domains: []types.Domain{ + { + Main: "foobar", + SANs: []string{"foobar", "foobar"}, + }, + { + Main: "foobar", + SANs: []string{"foobar", "foobar"}, + }, + }, + Options: "foobar", + }, + }, + }, + }, + { + desc: "simple TLS annotation", + annotations: map[string]string{ + "traefik.ingress.kubernetes.io/router.tls": "true", + }, + expected: &RouterConfig{ + Router: &RouterIng{ + PathMatcher: "PathPrefix", + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + { + desc: "empty map", + annotations: nil, + expected: nil, + }, + { + desc: "nil map", + annotations: nil, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + cfg, err := parseRouterConfig(test.annotations) + require.NoError(t, err) + + assert.Equal(t, test.expected, cfg) + }) + } +} + +func Test_parseServiceConfig(t *testing.T) { + testCases := []struct { + desc string + annotations map[string]string + expected *ServiceConfig + }{ + { + desc: "service annotations", + annotations: map[string]string{ + "ingress.kubernetes.io/foo": "bar", + "traefik.ingress.kubernetes.io/foo": "bar", + "traefik.ingress.kubernetes.io/service.serversscheme": "protocol", + "traefik.ingress.kubernetes.io/service.passhostheader": "true", + "traefik.ingress.kubernetes.io/service.sticky": "true", + "traefik.ingress.kubernetes.io/service.sticky.cookie.httponly": "true", + "traefik.ingress.kubernetes.io/service.sticky.cookie.name": "foobar", + "traefik.ingress.kubernetes.io/service.sticky.cookie.secure": "true", + }, + expected: &ServiceConfig{ + Service: &ServiceIng{ + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "foobar", + Secure: true, + HTTPOnly: true, + }, + }, + ServersScheme: "protocol", + PassHostHeader: Bool(true), + }, + }, + }, + { + desc: "simple sticky annotation", + annotations: map[string]string{ + "traefik.ingress.kubernetes.io/service.sticky": "true", + }, + expected: &ServiceConfig{ + Service: &ServiceIng{ + Sticky: &dynamic.Sticky{}, + PassHostHeader: Bool(true), + }, + }, + }, + { + desc: "empty map", + annotations: map[string]string{}, + expected: nil, + }, + { + desc: "nil map", + annotations: nil, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + cfg, err := parseServiceConfig(test.annotations) + require.NoError(t, err) + + assert.Equal(t, test.expected, cfg) + }) + } +} + +func Test_convertAnnotations(t *testing.T) { + testCases := []struct { + desc string + annotations map[string]string + expected map[string]string + }{ + { + desc: "router annotations", + annotations: map[string]string{ + "ingress.kubernetes.io/foo": "bar", + "traefik.ingress.kubernetes.io/foo": "bar", + "traefik.ingress.kubernetes.io/router.pathmatcher": "foobar", + "traefik.ingress.kubernetes.io/router.entrypoints": "foobar,foobar", + "traefik.ingress.kubernetes.io/router.middlewares": "foobar,foobar", + "traefik.ingress.kubernetes.io/router.priority": "42", + "traefik.ingress.kubernetes.io/router.tls": "true", + "traefik.ingress.kubernetes.io/router.tls.certresolver": "foobar", + "traefik.ingress.kubernetes.io/router.tls.domains.0.main": "foobar", + "traefik.ingress.kubernetes.io/router.tls.domains.0.sans": "foobar,foobar", + "traefik.ingress.kubernetes.io/router.tls.domains.1.main": "foobar", + "traefik.ingress.kubernetes.io/router.tls.domains.1.sans": "foobar,foobar", + "traefik.ingress.kubernetes.io/router.tls.options": "foobar", + }, + expected: map[string]string{ + "traefik.foo": "bar", + "traefik.router.pathmatcher": "foobar", + "traefik.router.entrypoints": "foobar,foobar", + "traefik.router.middlewares": "foobar,foobar", + "traefik.router.priority": "42", + "traefik.router.tls": "true", + "traefik.router.tls.certresolver": "foobar", + "traefik.router.tls.domains[0].main": "foobar", + "traefik.router.tls.domains[0].sans": "foobar,foobar", + "traefik.router.tls.domains[1].main": "foobar", + "traefik.router.tls.domains[1].sans": "foobar,foobar", + "traefik.router.tls.options": "foobar", + }, + }, + { + desc: "service annotations", + annotations: map[string]string{ + "traefik.ingress.kubernetes.io/service.serversscheme": "protocol", + "traefik.ingress.kubernetes.io/service.passhostheader": "true", + "traefik.ingress.kubernetes.io/service.sticky": "true", + "traefik.ingress.kubernetes.io/service.sticky.cookie.httponly": "true", + "traefik.ingress.kubernetes.io/service.sticky.cookie.name": "foobar", + "traefik.ingress.kubernetes.io/service.sticky.cookie.secure": "true", + }, + expected: map[string]string{ + "traefik.service.passhostheader": "true", + "traefik.service.serversscheme": "protocol", + "traefik.service.sticky": "true", + "traefik.service.sticky.cookie.httponly": "true", + "traefik.service.sticky.cookie.name": "foobar", + "traefik.service.sticky.cookie.secure": "true", + }, + }, + { + desc: "empty map", + annotations: map[string]string{}, + expected: nil, + }, + { + desc: "nil map", + annotations: nil, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + labels := convertAnnotations(test.annotations) + + assert.Equal(t, test.expected, labels) + }) + } +} diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_endpoint.yml new file mode 100644 index 000000000..b19cd5c3f --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_endpoint.yml @@ -0,0 +1,15 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 +- addresses: + - ip: 10.21.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_ingress.yml new file mode 100644 index 000000000..e851be017 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_ingress.yml @@ -0,0 +1,28 @@ +kind: Ingress +apiVersion: extensions/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + ingress.kubernetes.io/foo: bar + traefik.ingress.kubernetes.io/foo: bar + traefik.ingress.kubernetes.io/router.pathmatcher: Path + traefik.ingress.kubernetes.io/router.entrypoints: ep1,ep2 + traefik.ingress.kubernetes.io/router.middlewares: md1,md2 + traefik.ingress.kubernetes.io/router.priority: "42" + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.tls.certresolver: foobar + traefik.ingress.kubernetes.io/router.tls.domains.0.main: domain.com + traefik.ingress.kubernetes.io/router.tls.domains.0.sans: one.domain.com,two.domain.com + traefik.ingress.kubernetes.io/router.tls.domains.1.main: example.com + traefik.ingress.kubernetes.io/router.tls.domains.1.sans: one.example.com,two.example.com + traefik.ingress.kubernetes.io/router.tls.options: foobar + +spec: + rules: + - http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_service.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_service.yml new file mode 100644 index 000000000..e0de9f321 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations_service.yml @@ -0,0 +1,20 @@ +--- +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + annotations: + ingress.kubernetes.io/foo: bar + traefik.ingress.kubernetes.io/foo: bar + traefik.ingress.kubernetes.io/service.serversscheme: protocol + traefik.ingress.kubernetes.io/service.passhostheader: "true" + traefik.ingress.kubernetes.io/service.sticky: "true" + traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true" + traefik.ingress.kubernetes.io/service.sticky.cookie.name: foobar + traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true" + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 84268cca0..b77e348b9 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -29,6 +29,7 @@ import ( const ( annotationKubernetesIngressClass = "kubernetes.io/ingress.class" traefikDefaultIngressClass = "traefik" + defaultPathMatcher = "PathPrefix" ) // Provider holds configurations of the provider. @@ -173,96 +174,6 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } -func checkStringQuoteValidity(value string) error { - _, err := strconv.Unquote(`"` + value + `"`) - return err -} - -func loadService(client Client, namespace string, backend v1beta1.IngressBackend) (*dynamic.Service, error) { - service, exists, err := client.GetService(namespace, backend.ServiceName) - if err != nil { - return nil, err - } - - if !exists { - return nil, errors.New("service not found") - } - - var servers []dynamic.Server - var portName string - var portSpec corev1.ServicePort - var match bool - for _, p := range service.Spec.Ports { - if (backend.ServicePort.Type == intstr.Int && backend.ServicePort.IntVal == p.Port) || - (backend.ServicePort.Type == intstr.String && backend.ServicePort.StrVal == p.Name) { - portName = p.Name - portSpec = p - match = true - break - } - } - - if !match { - return nil, errors.New("service port not found") - } - - if service.Spec.Type == corev1.ServiceTypeExternalName { - protocol := "http" - if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { - protocol = "https" - } - - servers = append(servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, portSpec.Port), - }) - } else { - endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, backend.ServiceName) - if endpointsErr != nil { - return nil, endpointsErr - } - - if !endpointsExists { - return nil, errors.New("endpoints not found") - } - - if len(endpoints.Subsets) == 0 { - return nil, errors.New("subset not found") - } - - var port int32 - for _, subset := range endpoints.Subsets { - for _, p := range subset.Ports { - if portName == p.Name { - port = p.Port - break - } - } - - if port == 0 { - return nil, errors.New("cannot define a port") - } - - protocol := "http" - if portSpec.Port == 443 || strings.HasPrefix(portName, "https") { - protocol = "https" - } - - for _, addr := range subset.Addresses { - servers = append(servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port), - }) - } - } - } - - return &dynamic.Service{ - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: servers, - PassHostHeader: func(v bool) *bool { return &v }(true), - }, - }, nil -} - func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Client) *dynamic.Configuration { conf := &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ @@ -275,7 +186,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl ingresses := client.GetIngresses() - tlsConfigs := make(map[string]*tls.CertAndStores) + certConfigs := make(map[string]*tls.CertAndStores) for _, ingress := range ingresses { ctx = log.With(ctx, log.Str("ingress", ingress.Name), log.Str("namespace", ingress.Namespace)) @@ -283,35 +194,46 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl continue } - err := getTLS(ctx, ingress, client, tlsConfigs) + rtConfig, err := parseRouterConfig(ingress.Annotations) + if err != nil { + log.FromContext(ctx).Errorf("Failed to parse annotations: %v", err) + continue + } + + err = getCertificates(ctx, ingress, client, certConfigs) if err != nil { log.FromContext(ctx).Errorf("Error configuring TLS: %v", err) } - if len(ingress.Spec.Rules) == 0 { - if ingress.Spec.Backend != nil { - if _, ok := conf.HTTP.Services["default-backend"]; ok { - log.FromContext(ctx).Error("The default backend already exists.") - continue - } - - service, err := loadService(client, ingress.Namespace, *ingress.Spec.Backend) - if err != nil { - log.FromContext(ctx). - WithField("serviceName", ingress.Spec.Backend.ServiceName). - WithField("servicePort", ingress.Spec.Backend.ServicePort.String()). - Errorf("Cannot create service: %v", err) - continue - } - - conf.HTTP.Routers["default-router"] = &dynamic.Router{ - Rule: "PathPrefix(`/`)", - Priority: math.MinInt32, - Service: "default-backend", - } - - conf.HTTP.Services["default-backend"] = service + if len(ingress.Spec.Rules) == 0 && ingress.Spec.Backend != nil { + if _, ok := conf.HTTP.Services["default-backend"]; ok { + log.FromContext(ctx).Error("The default backend already exists.") + continue } + + service, err := loadService(client, ingress.Namespace, *ingress.Spec.Backend) + if err != nil { + log.FromContext(ctx). + WithField("serviceName", ingress.Spec.Backend.ServiceName). + WithField("servicePort", ingress.Spec.Backend.ServicePort.String()). + Errorf("Cannot create service: %v", err) + continue + } + + rt := &dynamic.Router{ + Rule: "PathPrefix(`/`)", + Priority: math.MinInt32, + Service: "default-backend", + } + + if rtConfig != nil && rtConfig.Router != nil { + rt.EntryPoints = rtConfig.Router.EntryPoints + rt.Middlewares = rtConfig.Router.Middlewares + rt.TLS = rtConfig.Router.TLS + } + + conf.HTTP.Routers["default-router"] = rt + conf.HTTP.Services["default-backend"] = service } for _, rule := range ingress.Spec.Rules { @@ -321,46 +243,26 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl } if rule.HTTP != nil { - for _, p := range rule.HTTP.Paths { - service, err := loadService(client, ingress.Namespace, p.Backend) + for _, pa := range rule.HTTP.Paths { + if err = checkStringQuoteValidity(pa.Path); err != nil { + log.FromContext(ctx).Errorf("Invalid syntax for path: %s", pa.Path) + continue + } + + service, err := loadService(client, ingress.Namespace, pa.Backend) if err != nil { log.FromContext(ctx). - WithField("serviceName", p.Backend.ServiceName). - WithField("servicePort", p.Backend.ServicePort.String()). + WithField("serviceName", pa.Backend.ServiceName). + WithField("servicePort", pa.Backend.ServicePort.String()). Errorf("Cannot create service: %v", err) continue } - if err = checkStringQuoteValidity(p.Path); err != nil { - log.FromContext(ctx).Errorf("Invalid syntax for path: %s", p.Path) - continue - } - - serviceName := provider.Normalize(ingress.Namespace + "-" + p.Backend.ServiceName + "-" + p.Backend.ServicePort.String()) - var rules []string - if len(rule.Host) > 0 { - rules = []string{"Host(`" + rule.Host + "`)"} - } - - if len(p.Path) > 0 { - rules = append(rules, "PathPrefix(`"+p.Path+"`)") - } - - routerKey := strings.TrimPrefix(provider.Normalize(rule.Host+p.Path), "-") - conf.HTTP.Routers[routerKey] = &dynamic.Router{ - Rule: strings.Join(rules, " && "), - Service: serviceName, - } - - if len(ingress.Spec.TLS) > 0 { - // TLS enabled for this ingress, add TLS router - conf.HTTP.Routers[routerKey+"-tls"] = &dynamic.Router{ - Rule: strings.Join(rules, " && "), - Service: serviceName, - TLS: &dynamic.RouterTLSConfig{}, - } - } + serviceName := provider.Normalize(ingress.Namespace + "-" + pa.Backend.ServiceName + "-" + pa.Backend.ServicePort.String()) conf.HTTP.Services[serviceName] = service + + routerKey := strings.TrimPrefix(provider.Normalize(rule.Host+pa.Path), "-") + conf.HTTP.Routers[routerKey] = loadRouter(ingress, rule, pa, rtConfig, serviceName) } } @@ -371,7 +273,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl } } - certs := getTLSConfig(tlsConfigs) + certs := getTLSConfig(certConfigs) if len(certs) > 0 { conf.TLS = &dynamic.TLSConfiguration{ Certificates: certs, @@ -381,96 +283,6 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl return conf } -func shouldProcessIngress(ingressClass string, ingressClassAnnotation string) bool { - return ingressClass == ingressClassAnnotation || - (len(ingressClass) == 0 && ingressClassAnnotation == traefikDefaultIngressClass) -} - -func getTLS(ctx context.Context, ingress *v1beta1.Ingress, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { - for _, t := range ingress.Spec.TLS { - if t.SecretName == "" { - log.FromContext(ctx).Debugf("Skipping TLS sub-section: No secret name provided") - continue - } - - configKey := ingress.Namespace + "-" + t.SecretName - if _, tlsExists := tlsConfigs[configKey]; !tlsExists { - secret, exists, err := k8sClient.GetSecret(ingress.Namespace, t.SecretName) - if err != nil { - return fmt.Errorf("failed to fetch secret %s/%s: %v", ingress.Namespace, t.SecretName, err) - } - if !exists { - return fmt.Errorf("secret %s/%s does not exist", ingress.Namespace, t.SecretName) - } - - cert, key, err := getCertificateBlocks(secret, ingress.Namespace, t.SecretName) - if err != nil { - return err - } - - tlsConfigs[configKey] = &tls.CertAndStores{ - Certificate: tls.Certificate{ - CertFile: tls.FileOrContent(cert), - KeyFile: tls.FileOrContent(key), - }, - } - } - } - - return nil -} - -func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores { - var secretNames []string - for secretName := range tlsConfigs { - secretNames = append(secretNames, secretName) - } - sort.Strings(secretNames) - - var configs []*tls.CertAndStores - for _, secretName := range secretNames { - configs = append(configs, tlsConfigs[secretName]) - } - - return configs -} - -func getCertificateBlocks(secret *corev1.Secret, namespace, secretName string) (string, string, error) { - var missingEntries []string - - tlsCrtData, tlsCrtExists := secret.Data["tls.crt"] - if !tlsCrtExists { - missingEntries = append(missingEntries, "tls.crt") - } - - tlsKeyData, tlsKeyExists := secret.Data["tls.key"] - if !tlsKeyExists { - missingEntries = append(missingEntries, "tls.key") - } - - if len(missingEntries) > 0 { - return "", "", fmt.Errorf("secret %s/%s is missing the following TLS data entries: %s", - namespace, secretName, strings.Join(missingEntries, ", ")) - } - - cert := string(tlsCrtData) - if cert == "" { - missingEntries = append(missingEntries, "tls.crt") - } - - key := string(tlsKeyData) - if key == "" { - missingEntries = append(missingEntries, "tls.key") - } - - if len(missingEntries) > 0 { - return "", "", fmt.Errorf("secret %s/%s contains the following empty TLS data entries: %s", - namespace, secretName, strings.Join(missingEntries, ", ")) - } - - return cert, key, nil -} - func (p *Provider) updateIngressStatus(i *v1beta1.Ingress, k8sClient Client) error { // Only process if an EndpointIngress has been configured if p.IngressEndpoint == nil { @@ -509,6 +321,245 @@ func (p *Provider) updateIngressStatus(i *v1beta1.Ingress, k8sClient Client) err return k8sClient.UpdateIngressStatus(i.Namespace, i.Name, service.Status.LoadBalancer.Ingress[0].IP, service.Status.LoadBalancer.Ingress[0].Hostname) } +func shouldProcessIngress(ingressClass string, ingressClassAnnotation string) bool { + return ingressClass == ingressClassAnnotation || + (len(ingressClass) == 0 && ingressClassAnnotation == traefikDefaultIngressClass) +} + +func getCertificates(ctx context.Context, ingress *v1beta1.Ingress, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { + for _, t := range ingress.Spec.TLS { + if t.SecretName == "" { + log.FromContext(ctx).Debugf("Skipping TLS sub-section: No secret name provided") + continue + } + + configKey := ingress.Namespace + "-" + t.SecretName + if _, tlsExists := tlsConfigs[configKey]; !tlsExists { + secret, exists, err := k8sClient.GetSecret(ingress.Namespace, t.SecretName) + if err != nil { + return fmt.Errorf("failed to fetch secret %s/%s: %v", ingress.Namespace, t.SecretName, err) + } + if !exists { + return fmt.Errorf("secret %s/%s does not exist", ingress.Namespace, t.SecretName) + } + + cert, key, err := getCertificateBlocks(secret, ingress.Namespace, t.SecretName) + if err != nil { + return err + } + + tlsConfigs[configKey] = &tls.CertAndStores{ + Certificate: tls.Certificate{ + CertFile: tls.FileOrContent(cert), + KeyFile: tls.FileOrContent(key), + }, + } + } + } + + return nil +} + +func getCertificateBlocks(secret *corev1.Secret, namespace, secretName string) (string, string, error) { + var missingEntries []string + + tlsCrtData, tlsCrtExists := secret.Data["tls.crt"] + if !tlsCrtExists { + missingEntries = append(missingEntries, "tls.crt") + } + + tlsKeyData, tlsKeyExists := secret.Data["tls.key"] + if !tlsKeyExists { + missingEntries = append(missingEntries, "tls.key") + } + + if len(missingEntries) > 0 { + return "", "", fmt.Errorf("secret %s/%s is missing the following TLS data entries: %s", + namespace, secretName, strings.Join(missingEntries, ", ")) + } + + cert := string(tlsCrtData) + if cert == "" { + missingEntries = append(missingEntries, "tls.crt") + } + + key := string(tlsKeyData) + if key == "" { + missingEntries = append(missingEntries, "tls.key") + } + + if len(missingEntries) > 0 { + return "", "", fmt.Errorf("secret %s/%s contains the following empty TLS data entries: %s", + namespace, secretName, strings.Join(missingEntries, ", ")) + } + + return cert, key, nil +} + +func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores { + var secretNames []string + for secretName := range tlsConfigs { + secretNames = append(secretNames, secretName) + } + sort.Strings(secretNames) + + var configs []*tls.CertAndStores + for _, secretName := range secretNames { + configs = append(configs, tlsConfigs[secretName]) + } + + return configs +} + +func loadService(client Client, namespace string, backend v1beta1.IngressBackend) (*dynamic.Service, error) { + service, exists, err := client.GetService(namespace, backend.ServiceName) + if err != nil { + return nil, err + } + + if !exists { + return nil, errors.New("service not found") + } + + var portName string + var portSpec corev1.ServicePort + var match bool + for _, p := range service.Spec.Ports { + if (backend.ServicePort.Type == intstr.Int && backend.ServicePort.IntVal == p.Port) || + (backend.ServicePort.Type == intstr.String && backend.ServicePort.StrVal == p.Name) { + portName = p.Name + portSpec = p + match = true + break + } + } + + if !match { + return nil, errors.New("service port not found") + } + + svc := &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: func(v bool) *bool { return &v }(true), + }, + } + + svcConfig, err := parseServiceConfig(service.Annotations) + if err != nil { + return nil, err + } + + if svcConfig != nil && svcConfig.Service != nil { + svc.LoadBalancer.Sticky = svcConfig.Service.Sticky + if svcConfig.Service.PassHostHeader != nil { + svc.LoadBalancer.PassHostHeader = svcConfig.Service.PassHostHeader + } + } + + if service.Spec.Type == corev1.ServiceTypeExternalName { + protocol := getProtocol(portSpec, portSpec.Name, svcConfig) + + svc.LoadBalancer.Servers = []dynamic.Server{ + {URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, portSpec.Port)}, + } + + return svc, nil + } + + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, backend.ServiceName) + if endpointsErr != nil { + return nil, endpointsErr + } + + if !endpointsExists { + return nil, errors.New("endpoints not found") + } + + if len(endpoints.Subsets) == 0 { + return nil, errors.New("subset not found") + } + + var port int32 + for _, subset := range endpoints.Subsets { + for _, p := range subset.Ports { + if portName == p.Name { + port = p.Port + break + } + } + + if port == 0 { + return nil, errors.New("cannot define a port") + } + + protocol := getProtocol(portSpec, portName, svcConfig) + + for _, addr := range subset.Addresses { + svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{ + URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port), + }) + } + } + + return svc, nil +} + +func getProtocol(portSpec corev1.ServicePort, portName string, svcConfig *ServiceConfig) string { + if svcConfig != nil && svcConfig.Service != nil && svcConfig.Service.ServersScheme != "" { + return svcConfig.Service.ServersScheme + } + + protocol := "http" + if portSpec.Port == 443 || strings.HasPrefix(portName, "https") { + protocol = "https" + } + + return protocol +} + +func loadRouter(ingress *v1beta1.Ingress, rule v1beta1.IngressRule, pa v1beta1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) *dynamic.Router { + var rules []string + if len(rule.Host) > 0 { + rules = []string{"Host(`" + rule.Host + "`)"} + } + + if len(pa.Path) > 0 { + matcher := defaultPathMatcher + if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" { + matcher = rtConfig.Router.PathMatcher + } + + rules = append(rules, fmt.Sprintf("%s(`%s`)", matcher, pa.Path)) + } + + rt := &dynamic.Router{ + Rule: strings.Join(rules, " && "), + Service: serviceName, + } + + if len(ingress.Spec.TLS) > 0 { + // TLS enabled for this ingress, add TLS router + rt.TLS = &dynamic.RouterTLSConfig{} + } + + if rtConfig != nil && rtConfig.Router != nil { + rt.Priority = rtConfig.Router.Priority + rt.EntryPoints = rtConfig.Router.EntryPoints + rt.Middlewares = rtConfig.Router.Middlewares + + if rtConfig.Router.TLS != nil { + rt.TLS = rtConfig.Router.TLS + } + } + + return rt +} + +func checkStringQuoteValidity(value string) error { + _, err := strconv.Unquote(`"` + value + `"`) + return err +} + func throttleEvents(ctx context.Context, throttleDuration time.Duration, stop chan bool, eventsChan <-chan interface{}) chan interface{} { if throttleDuration == 0 { return nil diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 66fca43db..f54ca2c50 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -11,6 +11,7 @@ import ( "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/provider" "github.com/containous/traefik/v2/pkg/tls" + "github.com/containous/traefik/v2/pkg/types" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" "k8s.io/api/extensions/v1beta1" @@ -79,6 +80,60 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, + { + desc: "Ingress with annotations", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "bar": { + Rule: "Path(`/bar`)", + EntryPoints: []string{"ep1", "ep2"}, + Service: "testing-service1-80", + Middlewares: []string{"md1", "md2"}, + Priority: 42, + TLS: &dynamic.RouterTLSConfig{ + CertResolver: "foobar", + Domains: []types.Domain{ + { + Main: "domain.com", + SANs: []string{"one.domain.com", "two.domain.com"}, + }, + { + Main: "example.com", + SANs: []string{"one.example.com", "two.example.com"}, + }, + }, + Options: "foobar", + }, + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "foobar", + Secure: true, + HTTPOnly: true, + }, + }, + Servers: []dynamic.Server{ + { + URL: "protocol://10.10.0.1:8080", + }, + { + URL: "protocol://10.21.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, { desc: "Ingress with two different rules with one path", expected: &dynamic.Configuration{ @@ -176,7 +231,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, - }, { + }, + { desc: "Ingress with one host without path", expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{}, @@ -700,10 +756,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { "example-com": { Rule: "Host(`example.com`)", Service: "testing-example-com-80", - }, - "example-com-tls": { - Rule: "Host(`example.com`)", - Service: "testing-example-com-80", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -967,7 +1019,7 @@ func generateTestFilename(suffix, desc string) string { return "./fixtures/" + strings.ReplaceAll(desc, " ", "-") + suffix + ".yml" } -func TestGetTLS(t *testing.T) { +func TestGetCertificates(t *testing.T) { testIngressWithoutHostname := buildIngress( iNamespace("testing"), iRules( @@ -1129,7 +1181,7 @@ func TestGetTLS(t *testing.T) { t.Parallel() tlsConfigs := map[string]*tls.CertAndStores{} - err := getTLS(context.Background(), test.ingress, test.client, tlsConfigs) + err := getCertificates(context.Background(), test.ingress, test.client, tlsConfigs) if test.errResult != "" { assert.EqualError(t, err, test.errResult)