diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index ae6826b56..056e49b6f 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -616,7 +616,7 @@ Kubernetes certificate authority file path (not needed for in-cluster client). Kubernetes server endpoint (required for external cluster client). `--providers.kubernetesingress.ingressclass`: -Value of kubernetes.io/ingress.class annotation to watch for. +Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for. `--providers.kubernetesingress.ingressendpoint.hostname`: Hostname used for Kubernetes Ingress endpoints. diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 9f37ea02e..1eb97890f 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -616,7 +616,7 @@ Kubernetes certificate authority file path (not needed for in-cluster client). Kubernetes server endpoint (required for external cluster client). `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_INGRESSCLASS`: -Value of kubernetes.io/ingress.class annotation to watch for. +Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for. `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_INGRESSENDPOINT_HOSTNAME`: Hostname used for Kubernetes Ingress endpoints. diff --git a/integration/fixtures/k8s/08-ingressclass.yml b/integration/fixtures/k8s/08-ingressclass.yml new file mode 100644 index 000000000..7ae47a208 --- /dev/null +++ b/integration/fixtures/k8s/08-ingressclass.yml @@ -0,0 +1,70 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: IngressClass +metadata: + name: traefik-keep +spec: + controller: traefik.io/ingress-controller + +--- +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "whoami-keep-route" +spec: + ingressClassName: "traefik-keep" + rules: + - host: "whoami.test.keep" + http: + paths: + - path: "/keep" + backend: + serviceName: "whoami" + servicePort: 80 + +--- +apiVersion: networking.k8s.io/v1beta1 +kind: IngressClass +metadata: + name: traefik-drop +spec: + controller: traefik.io/ingress-controller + +--- +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "whoami-drop-route" +spec: + ingressClassName: "traefik-drop" + rules: + - host: "whoami.test.drop" + http: + paths: + - path: "/drop" + backend: + serviceName: "whoami" + servicePort: 80 + +--- +apiVersion: networking.k8s.io/v1beta1 +kind: IngressClass +metadata: + name: traefik-not-ingress-controller +spec: + controller: not.tr43phic.io/ingress-controller + +--- +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "whoami-drop-ingress" +spec: + ingressClassName: "traefik-not-ingress-controller" + rules: + - host: "whoami.test.not.ingress" + http: + paths: + - path: "/ingress" + backend: + serviceName: "whoami" + servicePort: 80 diff --git a/integration/fixtures/k8s_ingressclass.toml b/integration/fixtures/k8s_ingressclass.toml new file mode 100644 index 000000000..51848e026 --- /dev/null +++ b/integration/fixtures/k8s_ingressclass.toml @@ -0,0 +1,16 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[api] + insecure = true + +[entryPoints] + [entryPoints.web] + address = ":8000" + +[providers.kubernetesIngress] + ingressClass = "traefik-keep" diff --git a/integration/k8s_test.go b/integration/k8s_test.go index b5f2b2093..e83f30ce4 100644 --- a/integration/k8s_test.go +++ b/integration/k8s_test.go @@ -119,6 +119,17 @@ func (s *K8sSuite) TestGatewayConfiguration(c *check.C) { testConfiguration(c, "testdata/rawdata-gateway.json", "8080") } +func (s *K8sSuite) TestIngressclass(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/k8s_ingressclass.toml")) + defer display(c) + + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer s.killCmd(cmd) + + testConfiguration(c, "testdata/rawdata-ingressclass.json", "8080") +} + func testConfiguration(c *check.C, path, apiPort string) { err := try.GetRequest("http://127.0.0.1:"+apiPort+"/api/entrypoints", 20*time.Second, try.BodyContains(`"name":"web"`)) c.Assert(err, checker.IsNil) diff --git a/integration/resources/compose/k8s.yml b/integration/resources/compose/k8s.yml index 83f313c29..01316d95c 100644 --- a/integration/resources/compose/k8s.yml +++ b/integration/resources/compose/k8s.yml @@ -1,5 +1,5 @@ server: - image: rancher/k3s:v1.17.2-k3s1 + image: rancher/k3s:v1.18.15-k3s1 command: server --disable-agent --no-deploy coredns --no-deploy servicelb --no-deploy traefik --no-deploy local-storage --no-deploy metrics-server --log /output/k3s.log environment: - K3S_CLUSTER_SECRET=somethingtotallyrandom @@ -12,7 +12,7 @@ server: - 6443:6443 node: - image: rancher/k3s:v1.17.2-k3s1 + image: rancher/k3s:v1.18.15-k3s1 privileged: true links: - server diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index ffa2315ec..920154fcc 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -49,6 +49,28 @@ "using": [ "web" ] + }, + "whoami-drop-route-default-whoami-test-drop-drop@kubernetes": { + "entryPoints": [ + "web" + ], + "service": "default-whoami-80", + "rule": "Host(`whoami.test.drop`) \u0026\u0026 PathPrefix(`/drop`)", + "status": "enabled", + "using": [ + "web" + ] + }, + "whoami-keep-route-default-whoami-test-keep-keep@kubernetes": { + "entryPoints": [ + "web" + ], + "service": "default-whoami-80", + "rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)", + "status": "enabled", + "using": [ + "web" + ] } }, "middlewares": { @@ -89,6 +111,28 @@ "dashboard@internal" ] }, + "default-whoami-80@kubernetes": { + "loadBalancer": { + "servers": [ + { + "url": "XXXX" + }, + { + "url": "XXXX" + } + ], + "passHostHeader": true + }, + "status": "enabled", + "usedBy": [ + "whoami-drop-route-default-whoami-test-drop-drop@kubernetes", + "whoami-keep-route-default-whoami-test-keep-keep@kubernetes" + ], + "serverStatus": { + "http://XXXX": "UP", + "http://XXXX": "UP" + } + }, "default-whoami-http@kubernetes": { "loadBalancer": { "servers": [ diff --git a/integration/testdata/rawdata-ingressclass.json b/integration/testdata/rawdata-ingressclass.json new file mode 100644 index 000000000..53db99cef --- /dev/null +++ b/integration/testdata/rawdata-ingressclass.json @@ -0,0 +1,106 @@ +{ + "routers": { + "api@internal": { + "entryPoints": [ + "traefik" + ], + "service": "api@internal", + "rule": "PathPrefix(`/api`)", + "priority": 2147483646, + "status": "enabled", + "using": [ + "traefik" + ] + }, + "dashboard@internal": { + "entryPoints": [ + "traefik" + ], + "middlewares": [ + "dashboard_redirect@internal", + "dashboard_stripprefix@internal" + ], + "service": "dashboard@internal", + "rule": "PathPrefix(`/`)", + "priority": 2147483645, + "status": "enabled", + "using": [ + "traefik" + ] + }, + "whoami-keep-route-default-whoami-test-keep-keep@kubernetes": { + "entryPoints": [ + "web" + ], + "service": "default-whoami-80", + "rule": "Host(`whoami.test.keep`) \u0026\u0026 PathPrefix(`/keep`)", + "status": "enabled", + "using": [ + "web" + ] + } + }, + "middlewares": { + "dashboard_redirect@internal": { + "redirectRegex": { + "regex": "^(http:\\/\\/(\\[[\\w:.]+\\]|[\\w\\._-]+)(:\\d+)?)\\/$", + "replacement": "${1}/dashboard/", + "permanent": true + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "dashboard_stripprefix@internal": { + "stripPrefix": { + "prefixes": [ + "/dashboard/", + "/dashboard" + ] + }, + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + } + }, + "services": { + "api@internal": { + "status": "enabled", + "usedBy": [ + "api@internal" + ] + }, + "dashboard@internal": { + "status": "enabled", + "usedBy": [ + "dashboard@internal" + ] + }, + "default-whoami-80@kubernetes": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.42.0.4:80" + }, + { + "url": "http://10.42.0.5:80" + } + ], + "passHostHeader": true + }, + "status": "enabled", + "usedBy": [ + "whoami-keep-route-default-whoami-test-keep-keep@kubernetes" + ], + "serverStatus": { + "http://10.42.0.4:80": "UP", + "http://10.42.0.5:80": "UP" + } + }, + "noop@internal": { + "status": "enabled" + } + } +} \ No newline at end of file diff --git a/pkg/provider/kubernetes/ingress/client.go b/pkg/provider/kubernetes/ingress/client.go index f05a85372..4a3018e2f 100644 --- a/pkg/provider/kubernetes/ingress/client.go +++ b/pkg/provider/kubernetes/ingress/client.go @@ -476,3 +476,16 @@ func supportsIngressClass(serverVersion *version.Version) bool { return ingressClassVersion.LessThanOrEqual(serverVersion) } + +// filterIngressClassByName return a slice containing ingressclasses with the correct name. +func filterIngressClassByName(ingressClassName string, ics []*networkingv1beta1.IngressClass) []*networkingv1beta1.IngressClass { + var ingressClasses []*networkingv1beta1.IngressClass + + for _, ic := range ics { + if ic.Name == ingressClassName { + ingressClasses = append(ingressClasses, ic) + } + } + + return ingressClasses +} diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_ingress.yml new file mode 100644 index 000000000..fb4eec709 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_ingress.yml @@ -0,0 +1,30 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing +spec: + ingressClassName: traefik-lb + rules: + - http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 + +--- +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing +spec: + ingressClassName: traefik-lb2 + rules: + - http: + paths: + - path: /foo + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_ingressclass.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_ingressclass.yml new file mode 100644 index 000000000..c0dd6d23a --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_ingressclass.yml @@ -0,0 +1,14 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: IngressClass +metadata: + name: traefik-lb2 +spec: + controller: traefik.io/ingress-controller + +--- +apiVersion: networking.k8s.io/v1beta1 +kind: IngressClass +metadata: + name: traefik-lb +spec: + controller: traefik.io/ingress-controller diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingressClasses-filter_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +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 4c6e82e6d..5cb6da5ca 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -42,7 +42,7 @@ type Provider struct { CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"` Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` - IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` + IngressClass string `description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"` ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` lastConfiguration safe.Safe @@ -198,7 +198,11 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl log.FromContext(ctx).Warnf("Failed to list ingress classes: %v", err) } - ingressClasses = ics + if p.IngressClass != "" { + ingressClasses = filterIngressClassByName(p.IngressClass, ics) + } else { + ingressClasses = ics + } } ingresses := client.GetIngresses() diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 16b7b3a61..4483c6b32 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -1273,6 +1273,35 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, + { + desc: "v18 Ingress with ingressClasses filter", + serverVersion: "v1.18", + ingressClass: "traefik-lb2", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-foo": { + Rule: "PathPrefix(`/foo`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range testCases {