From f30a52c2dc4d59b862e72dc5a57b0b45218c0d9e Mon Sep 17 00:00:00 2001 From: mpl Date: Thu, 14 Nov 2019 19:28:04 +0100 Subject: [PATCH] Support for all services kinds (and sticky) in CRD Co-authored-by: Jean-Baptiste Doumenjou Co-authored-by: Julien Salleyron --- .../dynamic-configuration/kubernetes-crd.yml | 98 +++ .../routing/providers/crd_traefikservice.yml | 13 + .../routing/providers/kubernetes-crd.md | 78 ++ docs/content/user-guides/crd-acme/01-crd.yml | 23 + integration/fixtures/k8s/01-crd.yml | 15 + .../k8s/06-ingressroute-traefikservices.yml | 62 ++ integration/fixtures/k8s_crd.toml | 1 - integration/fixtures/k8s_default.toml | 6 +- integration/k8s_test.go | 10 +- integration/testdata/rawdata-crd.json | 219 ++--- integration/testdata/rawdata-ingress.json | 8 +- pkg/provider/kubernetes/crd/client.go | 43 +- .../kubernetes/crd/client_mock_test.go | 17 + .../crd/fixtures/with_mirroring.yml | 96 +++ .../crd/fixtures/with_mirroring2.yml | 122 +++ .../crd/fixtures/with_namespaces.yml | 271 ++++++ .../crd/fixtures/with_services_lb0.yml | 63 ++ .../crd/fixtures/with_services_lb1.yml | 174 ++++ .../crd/fixtures/with_services_lb2.yml | 79 ++ .../crd/fixtures/with_services_lb3.yml | 128 +++ .../crd/fixtures/with_services_only.yml | 44 + ...ith_tls_options_and_specific_namespace.yml | 2 +- .../with_unknown_tls_options_namespace.yml | 2 +- .../v1alpha1/fake/fake_traefik_client.go | 4 + .../v1alpha1/fake/fake_traefikservice.go | 136 +++ .../traefik/v1alpha1/generated_expansion.go | 2 + .../typed/traefik/v1alpha1/traefik_client.go | 5 + .../typed/traefik/v1alpha1/traefikservice.go | 182 ++++ .../informers/externalversions/generic.go | 2 + .../traefik/v1alpha1/interface.go | 7 + .../traefik/v1alpha1/traefikservice.go | 97 +++ .../traefik/v1alpha1/expansion_generated.go | 8 + .../traefik/v1alpha1/traefikservice.go | 102 +++ pkg/provider/kubernetes/crd/kubernetes.go | 75 +- .../kubernetes/crd/kubernetes_http.go | 366 +++++--- .../kubernetes/crd/kubernetes_test.go | 778 ++++++++++++++++-- .../crd/traefik/v1alpha1/ingressroute.go | 64 +- .../crd/traefik/v1alpha1/ingressroutetcp.go | 11 +- .../crd/traefik/v1alpha1/register.go | 2 + .../crd/traefik/v1alpha1/service.go | 64 ++ .../traefik/v1alpha1/zz_generated.deepcopy.go | 217 ++++- pkg/provider/kubernetes/k8s/parser.go | 2 +- 42 files changed, 3344 insertions(+), 354 deletions(-) create mode 100644 docs/content/routing/providers/crd_traefikservice.yml create mode 100644 integration/fixtures/k8s/06-ingressroute-traefikservices.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_mirroring.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_mirroring2.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_namespaces.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_services_lb0.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_services_lb1.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_services_lb2.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_services_lb3.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_services_only.yml create mode 100644 pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefikservice.go create mode 100644 pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/traefikservice.go create mode 100644 pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/traefikservice.go create mode 100644 pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/traefikservice.go create mode 100644 pkg/provider/kubernetes/crd/traefik/v1alpha1/service.go diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd.yml index f7254e785..4c841827b 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd.yml @@ -56,6 +56,94 @@ spec: singular: ingressroutetcp scope: Namespaced +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: traefikservices.traefik.containo.us + +spec: + group: traefik.containo.us + version: v1alpha1 + names: + kind: TraefikService + plural: traefikservices + singular: traefikservice + scope: Namespaced + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr2 + namespace: default + +spec: + weighted: + services: + - name: s1 + weight: 1 + port: 80 + # Optional, as it is the default value + kind: Service + - name: s3 + weight: 1 + port: 80 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: wrr2 + kind: TraefikService + weight: 1 + - name: s3 + weight: 1 + port: 80 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror1 + namespace: default + +spec: + mirroring: + name: s1 + port: 80 + mirrors: + - name: s3 + percent: 20 + port: 80 + - name: mirror2 + kind: TraefikService + percent: 20 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror2 + namespace: default + +spec: + mirroring: + name: wrr2 + kind: TraefikService + mirrors: + - name: s2 + # Optional, as it is the default value + kind: Service + percent: 20 + port: 80 + --- apiVersion: traefik.containo.us/v1alpha1 kind: IngressRoute @@ -100,9 +188,19 @@ spec: - match: PathPrefix(`/misc`) services: - name: s3 + # Optional, as it is the default value + kind: Service port: 8443 # scheme allow to override the scheme for the service. (ex: https or h2c) scheme: https + - match: PathPrefix(`/lb`) + services: + - name: wrr1 + kind: TraefikService + - match: PathPrefix(`/mirrored`) + services: + - name: mirror1 + kind: TraefikService # use an empty tls object for TLS with Let's Encrypt tls: secretName: supersecret diff --git a/docs/content/routing/providers/crd_traefikservice.yml b/docs/content/routing/providers/crd_traefikservice.yml new file mode 100644 index 000000000..9d151625a --- /dev/null +++ b/docs/content/routing/providers/crd_traefikservice.yml @@ -0,0 +1,13 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: traefikservices.traefik.containo.us + +spec: + group: traefik.containo.us + version: v1alpha1 + names: + kind: TraefikService + plural: traefikservices + singular: traefikservice + scope: Namespaced diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index a8654701a..2b98b39d6 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -116,6 +116,84 @@ spec: More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md). +### Services + +If one needs a setup more sophisticated than a load-balancer of servers (which is a Kubernetes Service kind behind the scenes), +one can define and use additional service objects specific to Traefik, based on the `TraefikService` kind defined in the CRD below. + +```yaml +--8<-- "content/routing/providers/crd_traefikservice.yml" +``` + +Once the `TraefikService` kind has been registered with the Kubernetes cluster, it can then be used in `IngressRoute` definitions +(as well as recursively in other Traefik Services), such as below. +Note how the `name` field in the IngressRoute definition now refers to a TraefikService instead of a (Kubernetes) Service. +The reason this is allowed, and why a `name` can refer either to a TraefikService or a Service, +is because the `kind` field is used to break the ambiguity. The allowed values for this field are `TraefikService`, or `Service` +(which is the default value). + +```yaml +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: s2 + kind: Service + port: 80 + weight: 1 + - name: s3 + weight: 1 + port: 80 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror1 + namespace: default + +spec: + mirroring: + name: wrr1 + kind: TraefikService + mirrors: + - name: s1 + percent: 20 + port: 80 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: ingressroutebar + namespace: default + + +spec: + entryPoints: + - web + routes: + - match: Host(`bar.com`) && PathPrefix(`/foo`) + kind: Rule + services: + - name: mirror1 + namespace: default + kind: TraefikService +``` + +!!! important "References and namespaces" + + If the optional `namespace` attribute is not set, the configuration will be applied with the namespace of the current resource. + + Additionally, when the definition of the `TraefikService` is from another provider, + the cross-provider syntax (service@provider) should be used to refer to the `TraefikService`, just as in the middleware case. + Specifying a namespace attribute in this case would not make any sense, and will be ignored (except if the provider is `kubernetescrd`). + ### TLS Option Additionally, to allow for the use of TLS options in an IngressRoute, we defined the CRD below for the TLSOption kind. diff --git a/docs/content/user-guides/crd-acme/01-crd.yml b/docs/content/user-guides/crd-acme/01-crd.yml index 9aef075b4..9cc06829b 100644 --- a/docs/content/user-guides/crd-acme/01-crd.yml +++ b/docs/content/user-guides/crd-acme/01-crd.yml @@ -57,6 +57,21 @@ spec: singular: tlsoption scope: Namespaced +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: traefikservices.traefik.containo.us + +spec: + group: traefik.containo.us + version: v1alpha1 + names: + kind: TraefikService + plural: traefikservices + singular: traefikservice + scope: Namespaced + --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1beta1 @@ -120,6 +135,14 @@ rules: - get - list - watch + - apiGroups: + - traefik.containo.us + resources: + - traefikservices + verbs: + - get + - list + - watch --- kind: ClusterRoleBinding diff --git a/integration/fixtures/k8s/01-crd.yml b/integration/fixtures/k8s/01-crd.yml index 70b00f662..f696ac4cf 100644 --- a/integration/fixtures/k8s/01-crd.yml +++ b/integration/fixtures/k8s/01-crd.yml @@ -56,3 +56,18 @@ spec: plural: tlsoptions singular: tlsoption scope: Namespaced + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: traefikservices.traefik.containo.us + +spec: + group: traefik.containo.us + version: v1alpha1 + names: + kind: TraefikService + plural: traefikservices + singular: traefikservice + scope: Namespaced diff --git a/integration/fixtures/k8s/06-ingressroute-traefikservices.yml b/integration/fixtures/k8s/06-ingressroute-traefikservices.yml new file mode 100644 index 000000000..a8cee1bee --- /dev/null +++ b/integration/fixtures/k8s/06-ingressroute-traefikservices.yml @@ -0,0 +1,62 @@ +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror1 + namespace: default + +spec: + mirroring: + name: whoami + port: 80 + mirrors: + - name: whoami + port: 80 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: mirror1 + kind: TraefikService + - name: whoami + port: 80 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test3.route + namespace: default + +spec: + entryPoints: + - web + routes: + - match: Host(`foo.com`) && PathPrefix(`/wrr1`) + kind: Rule + services: + - name: wrr1 + kind: TraefikService +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: api.route + namespace: default + +spec: + entryPoints: + - web + routes: + - match: PathPrefix(`/api`) + kind: Rule + services: + - name: api@internal + kind: TraefikService diff --git a/integration/fixtures/k8s_crd.toml b/integration/fixtures/k8s_crd.toml index 891e3f13e..21bfa9855 100644 --- a/integration/fixtures/k8s_crd.toml +++ b/integration/fixtures/k8s_crd.toml @@ -6,7 +6,6 @@ level = "DEBUG" [api] - insecure = true [entryPoints] [entryPoints.footcp] diff --git a/integration/fixtures/k8s_default.toml b/integration/fixtures/k8s_default.toml index ede432aed..3c0d9e790 100644 --- a/integration/fixtures/k8s_default.toml +++ b/integration/fixtures/k8s_default.toml @@ -2,12 +2,12 @@ checkNewVersion = false sendAnonymousUsage = false -[api] - insecure = true - [log] level = "DEBUG" +[api] + insecure = true + [entryPoints] [entryPoints.web] address = ":8000" diff --git a/integration/k8s_test.go b/integration/k8s_test.go index 4a5deddeb..2cfa8ee7e 100644 --- a/integration/k8s_test.go +++ b/integration/k8s_test.go @@ -71,7 +71,7 @@ func (s *K8sSuite) TestIngressConfiguration(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - testConfiguration(c, "testdata/rawdata-ingress.json") + testConfiguration(c, "testdata/rawdata-ingress.json", "8080") } func (s *K8sSuite) TestCRDConfiguration(c *check.C) { @@ -82,11 +82,11 @@ func (s *K8sSuite) TestCRDConfiguration(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - testConfiguration(c, "testdata/rawdata-crd.json") + testConfiguration(c, "testdata/rawdata-crd.json", "8000") } -func testConfiguration(c *check.C, path string) { - err := try.GetRequest("http://127.0.0.1:8080/api/entrypoints", 20*time.Second, try.BodyContains(`"name":"web"`)) +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) expectedJSON := filepath.FromSlash(path) @@ -99,7 +99,7 @@ func testConfiguration(c *check.C, path string) { } var buf bytes.Buffer - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 40*time.Second, try.StatusCodeIs(http.StatusOK), matchesConfig(expectedJSON, &buf)) + err = try.GetRequest("http://127.0.0.1:"+apiPort+"/api/rawdata", 40*time.Second, try.StatusCodeIs(http.StatusOK), matchesConfig(expectedJSON, &buf)) if !*updateExpected { if err != nil { diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index 2418c781a..3aee24836 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -1,38 +1,21 @@ { "routers": { - "api@internal": { - "entryPoints": [ - "traefik" - ], - "service": "api@internal", - "rule": "PathPrefix(`/api`)", - "priority": 9223372036854775806, - "status": "enabled", - "using": [ - "traefik" - ] - }, - "dashboard@internal": { - "entryPoints": [ - "traefik" - ], - "middlewares": [ - "dashboard_redirect@internal", - "dashboard_stripprefix@internal" - ], - "service": "dashboard@internal", - "rule": "PathPrefix(`/`)", - "priority": 9223372036854775805, - "status": "enabled", - "using": [ - "traefik" - ] - }, - "default-test.route-6b204d94623b3df4370c@kubernetescrd": { + "default-api-route-29f28a463fb5d5ba16d2@kubernetescrd": { "entryPoints": [ "web" ], - "service": "default-test.route-6b204d94623b3df4370c", + "service": "api@internal", + "rule": "PathPrefix(`/api`)", + "status": "enabled", + "using": [ + "web" + ] + }, + "default-test-route-6b204d94623b3df4370c@kubernetescrd": { + "entryPoints": [ + "web" + ], + "service": "default-test-route-6b204d94623b3df4370c", "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/bar`)", "priority": 12, "tls": { @@ -43,45 +26,33 @@ "web" ] }, - "default-test2.route-23c7f4c450289ee29016@kubernetescrd": { + "default-test2-route-23c7f4c450289ee29016@kubernetescrd": { "entryPoints": [ "web" ], "middlewares": [ "default-mychain@kubernetescrd" ], - "service": "default-test2.route-23c7f4c450289ee29016", + "service": "default-test2-route-23c7f4c450289ee29016", "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/tobestripped`)", "status": "enabled", "using": [ "web" ] + }, + "default-test3-route-7d0ac22d3d8db4b82618@kubernetescrd": { + "entryPoints": [ + "web" + ], + "service": "default-wrr1", + "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/wrr1`)", + "status": "enabled", + "using": [ + "web" + ] } }, "middlewares": { - "dashboard_redirect@internal": { - "redirectRegex": { - "regex": "^(http:\\/\\/[^:]+(:\\d+)?)/$", - "replacement": "${1}/dashboard/", - "permanent": true - }, - "status": "enabled", - "usedBy": [ - "dashboard@internal" - ] - }, - "dashboard_stripprefix@internal": { - "stripPrefix": { - "prefixes": [ - "/dashboard/", - "/dashboard" - ] - }, - "status": "enabled", - "usedBy": [ - "dashboard@internal" - ] - }, "default-mychain@kubernetescrd": { "chain": { "middlewares": [ @@ -90,7 +61,7 @@ }, "status": "enabled", "usedBy": [ - "default-test2.route-23c7f4c450289ee29016@kubernetescrd" + "default-test2-route-23c7f4c450289ee29016@kubernetescrd" ] }, "default-stripprefix@kubernetescrd": { @@ -106,56 +77,100 @@ "api@internal": { "status": "enabled", "usedBy": [ - "api@internal" + "default-api-route-29f28a463fb5d5ba16d2@kubernetescrd" ] }, "dashboard@internal": { + "status": "enabled" + }, + "default-mirror1@kubernetescrd": { + "mirroring": { + "service": "default-whoami-80", + "mirrors": [ + { + "name": "default-whoami-80" + } + ] + }, + "status": "enabled" + }, + "default-test-route-6b204d94623b3df4370c@kubernetescrd": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.42.0.3:80" + }, + { + "url": "http://10.42.0.5:80" + } + ], + "passHostHeader": true + }, "status": "enabled", "usedBy": [ - "dashboard@internal" + "default-test-route-6b204d94623b3df4370c@kubernetescrd" + ], + "serverStatus": { + "http://10.42.0.3:80": "UP", + "http://10.42.0.5:80": "UP" + } + }, + "default-test2-route-23c7f4c450289ee29016@kubernetescrd": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.42.0.3:80" + }, + { + "url": "http://10.42.0.5:80" + } + ], + "passHostHeader": true + }, + "status": "enabled", + "usedBy": [ + "default-test2-route-23c7f4c450289ee29016@kubernetescrd" + ], + "serverStatus": { + "http://10.42.0.3:80": "UP", + "http://10.42.0.5:80": "UP" + } + }, + "default-whoami-80@kubernetescrd": { + "loadBalancer": { + "servers": [ + { + "url": "http://10.42.0.3:80" + }, + { + "url": "http://10.42.0.5:80" + } + ], + "passHostHeader": true + }, + "status": "enabled", + "serverStatus": { + "http://10.42.0.3:80": "UP", + "http://10.42.0.5:80": "UP" + } + }, + "default-wrr1@kubernetescrd": { + "weighted": { + "services": [ + { + "name": "default-mirror1", + "weight": 1 + }, + { + "name": "default-whoami-80", + "weight": 1 + } + ] + }, + "status": "enabled", + "usedBy": [ + "default-test3-route-7d0ac22d3d8db4b82618@kubernetescrd" ] - }, - "default-test.route-6b204d94623b3df4370c@kubernetescrd": { - "loadBalancer": { - "servers": [ - { - "url": "http://10.42.0.3:80" - }, - { - "url": "http://10.42.0.6:80" - } - ], - "passHostHeader": true - }, - "status": "enabled", - "usedBy": [ - "default-test.route-6b204d94623b3df4370c@kubernetescrd" - ], - "serverStatus": { - "http://10.42.0.3:80": "UP", - "http://10.42.0.6:80": "UP" - } - }, - "default-test2.route-23c7f4c450289ee29016@kubernetescrd": { - "loadBalancer": { - "servers": [ - { - "url": "http://10.42.0.3:80" - }, - { - "url": "http://10.42.0.6:80" - } - ], - "passHostHeader": true - }, - "status": "enabled", - "usedBy": [ - "default-test2.route-23c7f4c450289ee29016@kubernetescrd" - ], - "serverStatus": { - "http://10.42.0.3:80": "UP", - "http://10.42.0.6:80": "UP" - } } }, "tcpRouters": { @@ -181,10 +196,10 @@ "terminationDelay": 100, "servers": [ { - "address": "10.42.0.2:8080" + "address": "10.42.0.4:8080" }, { - "address": "10.42.0.5:8080" + "address": "10.42.0.6:8080" } ] }, diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index 23b825cc7..74c9acad9 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -99,10 +99,10 @@ "loadBalancer": { "servers": [ { - "url": "http://10.42.0.2:80" + "url": "http://10.42.0.4:80" }, { - "url": "http://10.42.0.3:80" + "url": "http://10.42.0.6:80" } ], "passHostHeader": true @@ -114,8 +114,8 @@ "whoami-test-whoami@kubernetes" ], "serverStatus": { - "http://10.42.0.2:80": "UP", - "http://10.42.0.3:80": "UP" + "http://10.42.0.4:80": "UP", + "http://10.42.0.6:80": "UP" } } } diff --git a/pkg/provider/kubernetes/crd/client.go b/pkg/provider/kubernetes/crd/client.go index e0f29a193..384b8e7d7 100644 --- a/pkg/provider/kubernetes/crd/client.go +++ b/pkg/provider/kubernetes/crd/client.go @@ -49,6 +49,8 @@ type Client interface { GetIngressRoutes() []*v1alpha1.IngressRoute GetIngressRouteTCPs() []*v1alpha1.IngressRouteTCP GetMiddlewares() []*v1alpha1.Middleware + GetTraefikService(namespace, name string) (*v1alpha1.TraefikService, bool, error) + GetTraefikServices() []*v1alpha1.TraefikService GetTLSOptions() []*v1alpha1.TLSOption GetIngresses() []*extensionsv1beta1.Ingress @@ -100,7 +102,7 @@ func newClientImpl(csKube *kubernetes.Clientset, csCrd *versioned.Clientset) *cl func newInClusterClient(endpoint string) (*clientWrapper, error) { config, err := rest.InClusterConfig() if err != nil { - return nil, fmt.Errorf("failed to create in-cluster configuration: %s", err) + return nil, fmt.Errorf("failed to create in-cluster configuration: %v", err) } if endpoint != "" { @@ -134,7 +136,7 @@ func newExternalClusterClient(endpoint, token, caFilePath string) (*clientWrappe if caFilePath != "" { caData, err := ioutil.ReadFile(caFilePath) if err != nil { - return nil, fmt.Errorf("failed to read CA file %s: %s", caFilePath, err) + return nil, fmt.Errorf("failed to read CA file %s: %v", caFilePath, err) } config.TLSClientConfig = rest.TLSClientConfig{CAData: caData} @@ -160,6 +162,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< factoryCrd.Traefik().V1alpha1().Middlewares().Informer().AddEventHandler(eventHandler) factoryCrd.Traefik().V1alpha1().IngressRouteTCPs().Informer().AddEventHandler(eventHandler) factoryCrd.Traefik().V1alpha1().TLSOptions().Informer().AddEventHandler(eventHandler) + factoryCrd.Traefik().V1alpha1().TraefikServices().Informer().AddEventHandler(eventHandler) factoryKube := informers.NewFilteredSharedInformerFactory(c.csKube, resyncPeriod, ns, nil) factoryKube.Extensions().V1beta1().Ingresses().Informer().AddEventHandler(eventHandler) @@ -207,7 +210,7 @@ func (c *clientWrapper) GetIngressRoutes() []*v1alpha1.IngressRoute { for ns, factory := range c.factoriesCrd { ings, err := factory.Traefik().V1alpha1().IngressRoutes().Lister().List(c.labelSelector) if err != nil { - log.Errorf("Failed to list ingresses in namespace %s: %s", ns, err) + log.Errorf("Failed to list ingress routes in namespace %s: %v", ns, err) } result = append(result, ings...) } @@ -221,7 +224,7 @@ func (c *clientWrapper) GetIngressRouteTCPs() []*v1alpha1.IngressRouteTCP { for ns, factory := range c.factoriesCrd { ings, err := factory.Traefik().V1alpha1().IngressRouteTCPs().Lister().List(c.labelSelector) if err != nil { - log.Errorf("Failed to list tcp ingresses in namespace %s: %s", ns, err) + log.Errorf("Failed to list tcp ingress routes in namespace %s: %v", ns, err) } result = append(result, ings...) } @@ -235,7 +238,7 @@ func (c *clientWrapper) GetMiddlewares() []*v1alpha1.Middleware { for ns, factory := range c.factoriesCrd { middlewares, err := factory.Traefik().V1alpha1().Middlewares().Lister().List(c.labelSelector) if err != nil { - log.Errorf("Failed to list middlewares in namespace %s: %s", ns, err) + log.Errorf("Failed to list middlewares in namespace %s: %v", ns, err) } result = append(result, middlewares...) } @@ -243,6 +246,32 @@ func (c *clientWrapper) GetMiddlewares() []*v1alpha1.Middleware { return result } +// GetTraefikService returns the named service from the given namespace. +func (c *clientWrapper) GetTraefikService(namespace, name string) (*v1alpha1.TraefikService, bool, error) { + if !c.isWatchedNamespace(namespace) { + return nil, false, fmt.Errorf("failed to get service %s/%s: namespace is not within watched namespaces", namespace, name) + } + + service, err := c.factoriesCrd[c.lookupNamespace(namespace)].Traefik().V1alpha1().TraefikServices().Lister().TraefikServices(namespace).Get(name) + exist, err := translateNotFoundError(err) + + return service, exist, err +} + +func (c *clientWrapper) GetTraefikServices() []*v1alpha1.TraefikService { + var result []*v1alpha1.TraefikService + + for ns, factory := range c.factoriesCrd { + ings, err := factory.Traefik().V1alpha1().TraefikServices().Lister().List(c.labelSelector) + if err != nil { + log.Errorf("Failed to list Traefik services in namespace %s: %v", ns, err) + } + result = append(result, ings...) + } + + return result +} + // GetTLSOptions func (c *clientWrapper) GetTLSOptions() []*v1alpha1.TLSOption { var result []*v1alpha1.TLSOption @@ -250,7 +279,7 @@ func (c *clientWrapper) GetTLSOptions() []*v1alpha1.TLSOption { for ns, factory := range c.factoriesCrd { options, err := factory.Traefik().V1alpha1().TLSOptions().Lister().List(c.labelSelector) if err != nil { - log.Errorf("Failed to list tls options in namespace %s: %s", ns, err) + log.Errorf("Failed to list tls options in namespace %s: %v", ns, err) } result = append(result, options...) } @@ -264,7 +293,7 @@ func (c *clientWrapper) GetIngresses() []*extensionsv1beta1.Ingress { for ns, factory := range c.factoriesKube { ings, err := factory.Extensions().V1beta1().Ingresses().Lister().List(c.labelSelector) if err != nil { - log.Errorf("Failed to list ingresses in namespace %s: %s", ns, err) + log.Errorf("Failed to list ingresses in namespace %s: %v", ns, err) } result = append(result, ings...) } diff --git a/pkg/provider/kubernetes/crd/client_mock_test.go b/pkg/provider/kubernetes/crd/client_mock_test.go index da85f03d8..ee2abf786 100644 --- a/pkg/provider/kubernetes/crd/client_mock_test.go +++ b/pkg/provider/kubernetes/crd/client_mock_test.go @@ -38,6 +38,7 @@ type clientMock struct { ingressRouteTCPs []*v1alpha1.IngressRouteTCP middlewares []*v1alpha1.Middleware tlsOptions []*v1alpha1.TLSOption + traefikServices []*v1alpha1.TraefikService watchChan chan interface{} } @@ -64,6 +65,8 @@ func newClientMock(paths ...string) clientMock { c.ingressRouteTCPs = append(c.ingressRouteTCPs, o) case *v1alpha1.Middleware: c.middlewares = append(c.middlewares, o) + case *v1alpha1.TraefikService: + c.traefikServices = append(c.traefikServices, o) case *v1alpha1.TLSOption: c.tlsOptions = append(c.tlsOptions, o) case *v1beta12.Ingress: @@ -91,6 +94,20 @@ func (c clientMock) GetMiddlewares() []*v1alpha1.Middleware { return c.middlewares } +func (c clientMock) GetTraefikService(namespace, name string) (*v1alpha1.TraefikService, bool, error) { + for _, svc := range c.traefikServices { + if svc.Namespace == namespace && svc.Name == name { + return svc, true, nil + } + } + + return nil, false, nil +} + +func (c clientMock) GetTraefikServices() []*v1alpha1.TraefikService { + return c.traefikServices +} + func (c clientMock) GetTLSOptions() []*v1alpha1.TLSOption { return c.tlsOptions } diff --git a/pkg/provider/kubernetes/crd/fixtures/with_mirroring.yml b/pkg/provider/kubernetes/crd/fixtures/with_mirroring.yml new file mode 100644 index 000000000..0999772d6 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_mirroring.yml @@ -0,0 +1,96 @@ +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami4 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.1 + - ip: 10.10.0.2 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami4 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami4 + +------ +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami5 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami5 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami5 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror1 + namespace: default + +spec: + mirroring: + name: whoami5 + kind: Service + port: 8080 + mirrors: + - name: whoami4 + kind: Service + percent: 50 + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: mirror1 + kind: TraefikService diff --git a/pkg/provider/kubernetes/crd/fixtures/with_mirroring2.yml b/pkg/provider/kubernetes/crd/fixtures/with_mirroring2.yml new file mode 100644 index 000000000..61f6ee4b1 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_mirroring2.yml @@ -0,0 +1,122 @@ +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami4 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.1 + - ip: 10.10.0.2 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami4 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami4 + +------ +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami5 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami5 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami5 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr2 + namespace: default + +spec: + weighted: + services: + - name: whoami5 + weight: 1 + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: whoami4 + weight: 1 + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror1 + namespace: default + +spec: + mirroring: + name: wrr1 + kind: TraefikService + mirrors: + - name: wrr2 + kind: TraefikService + percent: 30 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: mirror1 + kind: TraefikService diff --git a/pkg/provider/kubernetes/crd/fixtures/with_namespaces.yml b/pkg/provider/kubernetes/crd/fixtures/with_namespaces.yml new file mode 100644 index 000000000..efdb3dd08 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_namespaces.yml @@ -0,0 +1,271 @@ +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami6 + namespace: baz + +subsets: + - addresses: + - ip: 10.10.0.5 + - ip: 10.10.0.6 + ports: + - name: web + port: 8080 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami5 + namespace: foo + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: web + port: 8080 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami4 + namespace: foo + +subsets: + - addresses: + - ip: 10.10.0.1 + - ip: 10.10.0.2 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami6 + namespace: baz + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami6 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami5 + namespace: foo + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami5 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami4 + namespace: foo + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami4 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror1 + namespace: foo + +spec: + mirroring: + name: whoami5 + port: 8080 + namespace: foo + mirrors: + - name: whoami4 + port: 8080 + - name: whoami6 + port: 8080 + namespace: baz + - name: mirrored + kind: TraefikService + namespace: bar + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr2 + namespace: bar + +spec: + weighted: + services: + - name: whoami5 + namespace: foo + weight: 1 + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror2 + namespace: foo + +spec: + mirroring: + name: whoami5 + port: 8080 + mirrors: + - name: whoami4 + port: 8080 + - name: whoami6 + port: 8080 + namespace: baz + - name: mirrored + kind: TraefikService + namespace: bar + - name: wrr1 + kind: TraefikService + namespace: foo + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror3 + namespace: foo + +spec: + mirroring: + name: wrr1 + kind: TraefikService + namespace: foo + mirrors: + - name: whoami4 + port: 8080 + - name: whoami6 + port: 8080 + namespace: baz + - name: mirrored + kind: TraefikService + namespace: bar + - name: wrr1 + kind: TraefikService + namespace: foo + + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror4 + namespace: foo + +spec: + mirroring: + name: wrr1 + kind: TraefikService + mirrors: + - name: whoami4 + port: 8080 + - name: whoami6 + port: 8080 + namespace: baz + - name: mirrored + kind: TraefikService + namespace: bar + - name: wrr1 + kind: TraefikService + namespace: foo + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirrored + namespace: bar + +spec: + mirroring: + name: whoami6 + port: 8080 + namespace: baz + mirrors: + - name: whoami4 + percent: 50 + port: 8080 + namespace: foo + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: foo + +spec: + weighted: + services: + - name: whoami4 + port: 8080 + - name: whoami6 + port: 8080 + namespace: baz + - name: mirror1 + kind: TraefikService + - name: wrr2 + kind: TraefikService + namespace: bar + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: whoami6 + port: 8080 + namespace: baz + - name: wrr1 + kind: TraefikService + namespace: foo + - name: mirror2 + kind: TraefikService + namespace: foo + - name: mirror3 + kind: TraefikService + namespace: foo + - name: mirror4 + kind: TraefikService + namespace: foo diff --git a/pkg/provider/kubernetes/crd/fixtures/with_services_lb0.yml b/pkg/provider/kubernetes/crd/fixtures/with_services_lb0.yml new file mode 100644 index 000000000..13471f742 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_services_lb0.yml @@ -0,0 +1,63 @@ +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami5 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami5 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami5 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: whoami5 + kind: Service + weight: 1 + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: wrr1 + kind: TraefikService diff --git a/pkg/provider/kubernetes/crd/fixtures/with_services_lb1.yml b/pkg/provider/kubernetes/crd/fixtures/with_services_lb1.yml new file mode 100644 index 000000000..54dbd2fc2 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_services_lb1.yml @@ -0,0 +1,174 @@ +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami4 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.1 + - ip: 10.10.0.2 + ports: + - name: web + port: 80 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami5 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: web + port: 8080 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami6 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.5 + - ip: 10.10.0.6 + ports: + - name: web + port: 80 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami7 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.7 + - ip: 10.10.0.8 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami4 + namespace: default + +spec: + ports: + - name: web + port: 80 + selector: + app: containous + task: whoami4 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami5 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami5 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami6 + namespace: default + +spec: + ports: + - name: web + port: 80 + selector: + app: containous + task: whoami6 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami7 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami7 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: whoami4 + port: 80 + weight: 1 + - name: whoami5 + port: 8080 + weight: 1 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr2 + namespace: default + +spec: + weighted: + services: + - name: whoami6 + port: 80 + weight: 1 + - name: whoami7 + port: 8080 + weight: 1 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: wrr1 + kind: TraefikService + - name: wrr2 + kind: TraefikService diff --git a/pkg/provider/kubernetes/crd/fixtures/with_services_lb2.yml b/pkg/provider/kubernetes/crd/fixtures/with_services_lb2.yml new file mode 100644 index 000000000..3ba1bfb59 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_services_lb2.yml @@ -0,0 +1,79 @@ +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami5 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: web + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr2 + namespace: default + +spec: + weighted: + services: + - name: whoami5 + weight: 1 + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami5 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami5 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: wrr2 + kind: TraefikService + weight: 1 + - name: whoami5 + weight: 1 + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: wrr1 + kind: TraefikService diff --git a/pkg/provider/kubernetes/crd/fixtures/with_services_lb3.yml b/pkg/provider/kubernetes/crd/fixtures/with_services_lb3.yml new file mode 100644 index 000000000..a3067183b --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_services_lb3.yml @@ -0,0 +1,128 @@ +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami5 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: web + port: 8080 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami4 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.1 + - ip: 10.10.0.2 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami5 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami5 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami4 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami4 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr2 + namespace: default + +spec: + weighted: + services: + - name: whoami5 + weight: 1 + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: mirror1 + namespace: default + +spec: + mirroring: + name: whoami5 + port: 8080 + mirrors: + - name: whoami4 + percent: 50 + port: 8080 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: wrr2 + kind: TraefikService + weight: 1 + - name: whoami5 + weight: 1 + port: 8080 + - name: mirror1 + kind: TraefikService + weight: 1 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: wrr1 + kind: TraefikService diff --git a/pkg/provider/kubernetes/crd/fixtures/with_services_only.yml b/pkg/provider/kubernetes/crd/fixtures/with_services_only.yml new file mode 100644 index 000000000..eee0f6a28 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_services_only.yml @@ -0,0 +1,44 @@ +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoami5 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: web + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami5 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: containous + task: whoami5 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: default + +spec: + weighted: + services: + - name: whoami5 + kind: Service + weight: 1 + port: 8080 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_tls_options_and_specific_namespace.yml b/pkg/provider/kubernetes/crd/fixtures/with_tls_options_and_specific_namespace.yml index b870b3389..e7e56e43f 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_tls_options_and_specific_namespace.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_tls_options_and_specific_namespace.yml @@ -58,4 +58,4 @@ spec: tls: options: name: foo - namespace: myns \ No newline at end of file + namespace: myns diff --git a/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options_namespace.yml b/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options_namespace.yml index 69606c2db..133561482 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options_namespace.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options_namespace.yml @@ -29,4 +29,4 @@ spec: tls: options: name: foo - namespace: unknown \ No newline at end of file + namespace: unknown diff --git a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefik_client.go b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefik_client.go index aeb1ddd26..2df4d152e 100644 --- a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefik_client.go +++ b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefik_client.go @@ -52,6 +52,10 @@ func (c *FakeTraefikV1alpha1) TLSOptions(namespace string) v1alpha1.TLSOptionInt return &FakeTLSOptions{c, namespace} } +func (c *FakeTraefikV1alpha1) TraefikServices(namespace string) v1alpha1.TraefikServiceInterface { + return &FakeTraefikServices{c, namespace} +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeTraefikV1alpha1) RESTClient() rest.Interface { diff --git a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefikservice.go b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefikservice.go new file mode 100644 index 000000000..408a53fa3 --- /dev/null +++ b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefikservice.go @@ -0,0 +1,136 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016-2019 Containous SAS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeTraefikServices implements TraefikServiceInterface +type FakeTraefikServices struct { + Fake *FakeTraefikV1alpha1 + ns string +} + +var traefikservicesResource = schema.GroupVersionResource{Group: "traefik.containo.us", Version: "v1alpha1", Resource: "traefikservices"} + +var traefikservicesKind = schema.GroupVersionKind{Group: "traefik.containo.us", Version: "v1alpha1", Kind: "TraefikService"} + +// Get takes name of the traefikService, and returns the corresponding traefikService object, and an error if there is any. +func (c *FakeTraefikServices) Get(name string, options v1.GetOptions) (result *v1alpha1.TraefikService, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(traefikservicesResource, c.ns, name), &v1alpha1.TraefikService{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TraefikService), err +} + +// List takes label and field selectors, and returns the list of TraefikServices that match those selectors. +func (c *FakeTraefikServices) List(opts v1.ListOptions) (result *v1alpha1.TraefikServiceList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(traefikservicesResource, traefikservicesKind, c.ns, opts), &v1alpha1.TraefikServiceList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.TraefikServiceList{ListMeta: obj.(*v1alpha1.TraefikServiceList).ListMeta} + for _, item := range obj.(*v1alpha1.TraefikServiceList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested traefikServices. +func (c *FakeTraefikServices) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(traefikservicesResource, c.ns, opts)) + +} + +// Create takes the representation of a traefikService and creates it. Returns the server's representation of the traefikService, and an error, if there is any. +func (c *FakeTraefikServices) Create(traefikService *v1alpha1.TraefikService) (result *v1alpha1.TraefikService, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(traefikservicesResource, c.ns, traefikService), &v1alpha1.TraefikService{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TraefikService), err +} + +// Update takes the representation of a traefikService and updates it. Returns the server's representation of the traefikService, and an error, if there is any. +func (c *FakeTraefikServices) Update(traefikService *v1alpha1.TraefikService) (result *v1alpha1.TraefikService, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(traefikservicesResource, c.ns, traefikService), &v1alpha1.TraefikService{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TraefikService), err +} + +// Delete takes name of the traefikService and deletes it. Returns an error if one occurs. +func (c *FakeTraefikServices) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(traefikservicesResource, c.ns, name), &v1alpha1.TraefikService{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeTraefikServices) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(traefikservicesResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.TraefikServiceList{}) + return err +} + +// Patch applies the patch and returns the patched traefikService. +func (c *FakeTraefikServices) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.TraefikService, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(traefikservicesResource, c.ns, name, pt, data, subresources...), &v1alpha1.TraefikService{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TraefikService), err +} diff --git a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/generated_expansion.go b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/generated_expansion.go index 30a0f1a1b..db86edc42 100644 --- a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/generated_expansion.go +++ b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/generated_expansion.go @@ -33,3 +33,5 @@ type IngressRouteTCPExpansion interface{} type MiddlewareExpansion interface{} type TLSOptionExpansion interface{} + +type TraefikServiceExpansion interface{} diff --git a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/traefik_client.go b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/traefik_client.go index 51c42b06e..ff594d739 100644 --- a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/traefik_client.go +++ b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/traefik_client.go @@ -38,6 +38,7 @@ type TraefikV1alpha1Interface interface { IngressRouteTCPsGetter MiddlewaresGetter TLSOptionsGetter + TraefikServicesGetter } // TraefikV1alpha1Client is used to interact with features provided by the traefik.containo.us group. @@ -61,6 +62,10 @@ func (c *TraefikV1alpha1Client) TLSOptions(namespace string) TLSOptionInterface return newTLSOptions(c, namespace) } +func (c *TraefikV1alpha1Client) TraefikServices(namespace string) TraefikServiceInterface { + return newTraefikServices(c, namespace) +} + // NewForConfig creates a new TraefikV1alpha1Client for the given config. func NewForConfig(c *rest.Config) (*TraefikV1alpha1Client, error) { config := *c diff --git a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/traefikservice.go b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/traefikservice.go new file mode 100644 index 000000000..db2969218 --- /dev/null +++ b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/traefikservice.go @@ -0,0 +1,182 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016-2019 Containous SAS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "time" + + scheme "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/generated/clientset/versioned/scheme" + v1alpha1 "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// TraefikServicesGetter has a method to return a TraefikServiceInterface. +// A group's client should implement this interface. +type TraefikServicesGetter interface { + TraefikServices(namespace string) TraefikServiceInterface +} + +// TraefikServiceInterface has methods to work with TraefikService resources. +type TraefikServiceInterface interface { + Create(*v1alpha1.TraefikService) (*v1alpha1.TraefikService, error) + Update(*v1alpha1.TraefikService) (*v1alpha1.TraefikService, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.TraefikService, error) + List(opts v1.ListOptions) (*v1alpha1.TraefikServiceList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.TraefikService, err error) + TraefikServiceExpansion +} + +// traefikServices implements TraefikServiceInterface +type traefikServices struct { + client rest.Interface + ns string +} + +// newTraefikServices returns a TraefikServices +func newTraefikServices(c *TraefikV1alpha1Client, namespace string) *traefikServices { + return &traefikServices{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the traefikService, and returns the corresponding traefikService object, and an error if there is any. +func (c *traefikServices) Get(name string, options v1.GetOptions) (result *v1alpha1.TraefikService, err error) { + result = &v1alpha1.TraefikService{} + err = c.client.Get(). + Namespace(c.ns). + Resource("traefikservices"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of TraefikServices that match those selectors. +func (c *traefikServices) List(opts v1.ListOptions) (result *v1alpha1.TraefikServiceList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.TraefikServiceList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("traefikservices"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested traefikServices. +func (c *traefikServices) Watch(opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("traefikservices"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch() +} + +// Create takes the representation of a traefikService and creates it. Returns the server's representation of the traefikService, and an error, if there is any. +func (c *traefikServices) Create(traefikService *v1alpha1.TraefikService) (result *v1alpha1.TraefikService, err error) { + result = &v1alpha1.TraefikService{} + err = c.client.Post(). + Namespace(c.ns). + Resource("traefikservices"). + Body(traefikService). + Do(). + Into(result) + return +} + +// Update takes the representation of a traefikService and updates it. Returns the server's representation of the traefikService, and an error, if there is any. +func (c *traefikServices) Update(traefikService *v1alpha1.TraefikService) (result *v1alpha1.TraefikService, err error) { + result = &v1alpha1.TraefikService{} + err = c.client.Put(). + Namespace(c.ns). + Resource("traefikservices"). + Name(traefikService.Name). + Body(traefikService). + Do(). + Into(result) + return +} + +// Delete takes name of the traefikService and deletes it. Returns an error if one occurs. +func (c *traefikServices) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("traefikservices"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *traefikServices) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + var timeout time.Duration + if listOptions.TimeoutSeconds != nil { + timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("traefikservices"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Timeout(timeout). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched traefikService. +func (c *traefikServices) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.TraefikService, err error) { + result = &v1alpha1.TraefikService{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("traefikservices"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/provider/kubernetes/crd/generated/informers/externalversions/generic.go b/pkg/provider/kubernetes/crd/generated/informers/externalversions/generic.go index daee522b5..376f3b4f4 100644 --- a/pkg/provider/kubernetes/crd/generated/informers/externalversions/generic.go +++ b/pkg/provider/kubernetes/crd/generated/informers/externalversions/generic.go @@ -69,6 +69,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Traefik().V1alpha1().Middlewares().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("tlsoptions"): return &genericInformer{resource: resource.GroupResource(), informer: f.Traefik().V1alpha1().TLSOptions().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("traefikservices"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Traefik().V1alpha1().TraefikServices().Informer()}, nil } diff --git a/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/interface.go b/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/interface.go index 06e742230..0a9c6d298 100644 --- a/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/interface.go +++ b/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/interface.go @@ -40,6 +40,8 @@ type Interface interface { Middlewares() MiddlewareInformer // TLSOptions returns a TLSOptionInformer. TLSOptions() TLSOptionInformer + // TraefikServices returns a TraefikServiceInformer. + TraefikServices() TraefikServiceInformer } type version struct { @@ -72,3 +74,8 @@ func (v *version) Middlewares() MiddlewareInformer { func (v *version) TLSOptions() TLSOptionInformer { return &tLSOptionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// TraefikServices returns a TraefikServiceInformer. +func (v *version) TraefikServices() TraefikServiceInformer { + return &traefikServiceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/traefikservice.go b/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/traefikservice.go new file mode 100644 index 000000000..14953352f --- /dev/null +++ b/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/traefikservice.go @@ -0,0 +1,97 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016-2019 Containous SAS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" + + versioned "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/generated/clientset/versioned" + internalinterfaces "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/generated/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1" + traefikv1alpha1 "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// TraefikServiceInformer provides access to a shared informer and lister for +// TraefikServices. +type TraefikServiceInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.TraefikServiceLister +} + +type traefikServiceInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewTraefikServiceInformer constructs a new informer for TraefikService type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTraefikServiceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredTraefikServiceInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredTraefikServiceInformer constructs a new informer for TraefikService type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredTraefikServiceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.TraefikV1alpha1().TraefikServices(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.TraefikV1alpha1().TraefikServices(namespace).Watch(options) + }, + }, + &traefikv1alpha1.TraefikService{}, + resyncPeriod, + indexers, + ) +} + +func (f *traefikServiceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredTraefikServiceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *traefikServiceInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&traefikv1alpha1.TraefikService{}, f.defaultInformer) +} + +func (f *traefikServiceInformer) Lister() v1alpha1.TraefikServiceLister { + return v1alpha1.NewTraefikServiceLister(f.Informer().GetIndexer()) +} diff --git a/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/expansion_generated.go b/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/expansion_generated.go index dc40e3ba8..13d6d076b 100644 --- a/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/expansion_generated.go +++ b/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/expansion_generated.go @@ -57,3 +57,11 @@ type TLSOptionListerExpansion interface{} // TLSOptionNamespaceListerExpansion allows custom methods to be added to // TLSOptionNamespaceLister. type TLSOptionNamespaceListerExpansion interface{} + +// TraefikServiceListerExpansion allows custom methods to be added to +// TraefikServiceLister. +type TraefikServiceListerExpansion interface{} + +// TraefikServiceNamespaceListerExpansion allows custom methods to be added to +// TraefikServiceNamespaceLister. +type TraefikServiceNamespaceListerExpansion interface{} diff --git a/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/traefikservice.go b/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/traefikservice.go new file mode 100644 index 000000000..807d76b40 --- /dev/null +++ b/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/traefikservice.go @@ -0,0 +1,102 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016-2019 Containous SAS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// TraefikServiceLister helps list TraefikServices. +type TraefikServiceLister interface { + // List lists all TraefikServices in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.TraefikService, err error) + // TraefikServices returns an object that can list and get TraefikServices. + TraefikServices(namespace string) TraefikServiceNamespaceLister + TraefikServiceListerExpansion +} + +// traefikServiceLister implements the TraefikServiceLister interface. +type traefikServiceLister struct { + indexer cache.Indexer +} + +// NewTraefikServiceLister returns a new TraefikServiceLister. +func NewTraefikServiceLister(indexer cache.Indexer) TraefikServiceLister { + return &traefikServiceLister{indexer: indexer} +} + +// List lists all TraefikServices in the indexer. +func (s *traefikServiceLister) List(selector labels.Selector) (ret []*v1alpha1.TraefikService, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.TraefikService)) + }) + return ret, err +} + +// TraefikServices returns an object that can list and get TraefikServices. +func (s *traefikServiceLister) TraefikServices(namespace string) TraefikServiceNamespaceLister { + return traefikServiceNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// TraefikServiceNamespaceLister helps list and get TraefikServices. +type TraefikServiceNamespaceLister interface { + // List lists all TraefikServices in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.TraefikService, err error) + // Get retrieves the TraefikService from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.TraefikService, error) + TraefikServiceNamespaceListerExpansion +} + +// traefikServiceNamespaceLister implements the TraefikServiceNamespaceLister +// interface. +type traefikServiceNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all TraefikServices in the indexer for a given namespace. +func (s traefikServiceNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.TraefikService, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.TraefikService)) + }) + return ret, err +} + +// Get retrieves the TraefikService from the indexer for a given namespace and name. +func (s traefikServiceNamespaceLister) Get(name string) (*v1alpha1.TraefikService, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("traefikservice"), name) + } + return obj.(*v1alpha1.TraefikService), nil +} diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 7392c22ad..6b9c6f332 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -16,6 +16,7 @@ import ( "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/job" "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/provider" "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" "github.com/containous/traefik/v2/pkg/safe" "github.com/containous/traefik/v2/pkg/tls" @@ -30,6 +31,11 @@ const ( traefikDefaultIngressClass = "traefik" ) +const ( + providerName = "kubernetescrd" + providerNamespaceSeparator = "@" +) + // Provider holds configurations of the provider. type Provider struct { Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` @@ -52,7 +58,7 @@ func (p *Provider) newK8sClient(ctx context.Context, labelSelector string) (*cli withEndpoint := "" if p.Endpoint != "" { - withEndpoint = fmt.Sprintf(" with endpoint %v", p.Endpoint) + withEndpoint = fmt.Sprintf(" with endpoint %s", p.Endpoint) } var client *clientWrapper @@ -83,7 +89,7 @@ func (p *Provider) Init() error { // Provide allows the k8s provider to provide configurations to traefik // using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { - ctxLog := log.With(context.Background(), log.Str(log.ProviderName, "kubernetescrd")) + ctxLog := log.With(context.Background(), log.Str(log.ProviderName, providerName)) logger := log.FromContext(ctxLog) logger.Debugf("Using label selector: %q", p.LabelSelector) @@ -120,11 +126,9 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. case <-stop: return nil case event := <-eventsChan: - // Note that event is the *first* event that came in during this - // throttling interval -- if we're hitting our throttle, we may have - // dropped events. This is fine, because we don't treat different - // event types differently. But if we do in the future, we'll need to - // track more information about the dropped events. + // Note that event is the *first* event that came in during this throttling interval -- if we're hitting our throttle, we may have dropped events. + // This is fine, because we don't treat different event types differently. + // But if we do in the future, we'll need to track more information about the dropped events. conf := p.loadConfigurationFromCRD(ctxLog, k8sClient) confHash, err := hashstructure.Hash(conf, nil) @@ -136,25 +140,25 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. default: p.lastConfiguration.Set(confHash) configurationChan <- dynamic.Message{ - ProviderName: "kubernetescrd", + ProviderName: providerName, Configuration: conf, } } - // If we're throttling, we sleep here for the throttle duration to - // enforce that we don't refresh faster than our throttle. time.Sleep - // returns immediately if p.ThrottleDuration is 0 (no throttle). + // If we're throttling, + // we sleep here for the throttle duration to enforce that we don't refresh faster than our throttle. + // time.Sleep returns immediately if p.ThrottleDuration is 0 (no throttle). time.Sleep(throttleDuration) } } } notify := func(err error, time time.Duration) { - logger.Errorf("Provider connection error: %s; retrying in %s", err, time) + logger.Errorf("Provider connection error: %v; retrying in %s", err, time) } err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) if err != nil { - logger.Errorf("Cannot connect to Provider: %s", err) + logger.Errorf("Cannot connect to Provider: %v", err) } }) @@ -173,7 +177,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) } for _, middleware := range client.GetMiddlewares() { - id := makeID(middleware.Namespace, middleware.Name) + id := provider.Normalize(makeID(middleware.Namespace, middleware.Name)) ctxMid := log.With(ctx, log.Str(log.MiddlewareName, id)) basicAuth, err := createBasicAuthMiddleware(client, middleware.Namespace, middleware.Spec.BasicAuth) @@ -231,6 +235,16 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) } } + cb := configBuilder{client} + for _, service := range client.GetTraefikServices() { + err := cb.buildTraefikService(ctx, service, conf.HTTP.Services) + if err != nil { + log.FromContext(ctx).WithField(log.ServiceName, service.Name). + Errorf("Error while building TraefikService: %v", err) + continue + } + } + return conf } @@ -244,7 +258,7 @@ func createErrorPageMiddleware(client Client, namespace string, errorPage *v1alp Query: errorPage.Query, } - balancerServerHTTP, err := createLoadBalancerServerHTTP(client, namespace, errorPage.Service) + balancerServerHTTP, err := configBuilder{client}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec) if err != nil { return nil, nil, err } @@ -286,7 +300,7 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *v1alp if len(auth.TLS.CertSecret) > 0 { authSecretCert, authSecretKey, err := loadAuthTLSSecret(namespace, auth.TLS.CertSecret, k8sClient) if err != nil { - return nil, fmt.Errorf("failed to load auth secret: %s", err) + return nil, fmt.Errorf("failed to load auth secret: %v", err) } forwardAuth.TLS.Cert = authSecretCert forwardAuth.TLS.Key = authSecretKey @@ -298,7 +312,7 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *v1alp func loadCASecret(namespace, secretName string, k8sClient Client) (string, error) { secret, ok, err := k8sClient.GetSecret(namespace, secretName) if err != nil { - return "", fmt.Errorf("failed to fetch secret '%s/%s': %s", namespace, secretName, err) + return "", fmt.Errorf("failed to fetch secret '%s/%s': %v", namespace, secretName, err) } if !ok { return "", fmt.Errorf("secret '%s/%s' not found", namespace, secretName) @@ -377,7 +391,7 @@ func getAuthCredentials(k8sClient Client, authSecret, namespace string) ([]strin auth, err := loadAuthCredentials(namespace, authSecret, k8sClient) if err != nil { - return nil, fmt.Errorf("failed to load auth credentials: %s", err) + return nil, fmt.Errorf("failed to load auth credentials: %v", err) } return auth, nil @@ -386,7 +400,7 @@ func getAuthCredentials(k8sClient Client, authSecret, namespace string) ([]strin func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]string, error) { secret, ok, err := k8sClient.GetSecret(namespace, secretName) if err != nil { - return nil, fmt.Errorf("failed to fetch secret '%s/%s': %s", namespace, secretName, err) + return nil, fmt.Errorf("failed to fetch secret '%s/%s': %v", namespace, secretName, err) } if !ok { return nil, fmt.Errorf("secret '%s/%s' not found", namespace, secretName) @@ -412,7 +426,7 @@ func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]stri } } if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("error reading secret for %v/%v: %v", namespace, secretName, err) + return nil, fmt.Errorf("error reading secret for %s/%s: %v", namespace, secretName, err) } if len(credentials) == 0 { return nil, fmt.Errorf("secret '%s/%s' does not contain any credentials", namespace, secretName) @@ -428,7 +442,7 @@ func createChainMiddleware(ctx context.Context, namespace string, chain *v1alpha var mds []string for _, mi := range chain.Middlewares { - if strings.Contains(mi.Name, "@") { + if strings.Contains(mi.Name, providerNamespaceSeparator) { if len(mi.Namespace) > 0 { log.FromContext(ctx). Warnf("namespace %q is ignored in cross-provider context", mi.Namespace) @@ -600,14 +614,12 @@ func getCertificateBlocks(secret *corev1.Secret, namespace, secretName string) ( func getCABlocks(secret *corev1.Secret, namespace, secretName string) (string, error) { tlsCrtData, tlsCrtExists := secret.Data["tls.ca"] if !tlsCrtExists { - return "", fmt.Errorf("the tls.ca entry is missing from secret %s/%s", - namespace, secretName) + return "", fmt.Errorf("the tls.ca entry is missing from secret %s/%s", namespace, secretName) } cert := string(tlsCrtData) if cert == "" { - return "", fmt.Errorf("the tls.ca entry in secret %s/%s is empty", - namespace, secretName) + return "", fmt.Errorf("the tls.ca entry in secret %s/%s is empty", namespace, secretName) } return cert, nil @@ -620,10 +632,9 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, stop ch // Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling) eventsChanBuffered := make(chan interface{}, 1) - // Run a goroutine that reads events from eventChan and does a - // non-blocking write to pendingEvent. This guarantees that writing to - // eventChan will never block, and that pendingEvent will have - // something in it if there's been an event since we read from that channel. + // Run a goroutine that reads events from eventChan and does a non-blocking write to pendingEvent. + // This guarantees that writing to eventChan will never block, + // and that pendingEvent will have something in it if there's been an event since we read from that channel. go func() { for { select { @@ -633,10 +644,8 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, stop ch select { case eventsChanBuffered <- nextEvent: default: - // We already have an event in eventsChanBuffered, so we'll - // do a refresh as soon as our throttle allows us to. It's fine - // to drop the event and keep whatever's in the buffer -- we - // don't do different things for different events + // We already have an event in eventsChanBuffered, so we'll do a refresh as soon as our throttle allows us to. + // It's fine to drop the event and keep whatever's in the buffer -- we don't do different things for different events log.FromContext(ctx).Debugf("Dropping event kind %T due to throttling", nextEvent) } } diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index bd894fe3d..0adca3e75 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -8,11 +8,18 @@ import ( "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/provider" "github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" "github.com/containous/traefik/v2/pkg/tls" corev1 "k8s.io/api/core/v1" ) +const ( + roundRobinStrategy = "RoundRobin" + httpsProtocol = "https" + httpProtocol = "http" +) + func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Client, tlsConfigs map[string]*tls.CertAndStores) *dynamic.HTTPConfiguration { conf := &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, @@ -39,6 +46,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli ingressName = ingressRoute.GenerateName } + cb := configBuilder{client} for _, route := range ingressRoute.Spec.Routes { if route.Kind != "Rule" { logger.Errorf("Unsupported match kind: %s. Only \"Rule\" is supported for now.", route.Kind) @@ -55,49 +63,15 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli continue } - key, err := makeServiceKey(route.Match, ingressName) + serviceKey, err := makeServiceKey(route.Match, ingressName) if err != nil { logger.Error(err) continue } - serviceName := makeID(ingressRoute.Namespace, key) - - for _, service := range route.Services { - balancerServerHTTP, err := createLoadBalancerServerHTTP(client, ingressRoute.Namespace, service) - if err != nil { - logger. - WithField("serviceName", service.Name). - WithField("servicePort", service.Port). - Errorf("Cannot create service: %v", err) - continue - } - - // If there is only one service defined, we skip the creation of the load balancer of services, - // i.e. the service on top is directly a load balancer of servers. - if len(route.Services) == 1 { - conf.Services[serviceName] = balancerServerHTTP - break - } - - serviceKey := fmt.Sprintf("%s-%s-%d", serviceName, service.Name, service.Port) - conf.Services[serviceKey] = balancerServerHTTP - - srv := dynamic.WRRService{Name: serviceKey} - srv.SetDefaults() - if service.Weight != nil { - srv.Weight = service.Weight - } - - if conf.Services[serviceName] == nil { - conf.Services[serviceName] = &dynamic.Service{Weighted: &dynamic.WeightedRoundRobin{}} - } - conf.Services[serviceName].Weighted.Services = append(conf.Services[serviceName].Weighted.Services, srv) - } - var mds []string for _, mi := range route.Middlewares { - if strings.Contains(mi.Name, "@") { + if strings.Contains(mi.Name, providerNamespaceSeparator) { if len(mi.Namespace) > 0 { logger. WithField(log.MiddlewareName, mi.Name). @@ -114,7 +88,36 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli mds = append(mds, makeID(ns, mi.Name)) } - conf.Routers[serviceName] = &dynamic.Router{ + normalized := provider.Normalize(makeID(ingressRoute.Namespace, serviceKey)) + serviceName := normalized + + if len(route.Services) > 1 { + spec := v1alpha1.ServiceSpec{ + Weighted: &v1alpha1.WeightedRoundRobin{ + Services: route.Services, + }, + } + + errBuild := cb.buildServicesLB(ctx, ingressRoute.Namespace, spec, serviceName, conf.Services) + if errBuild != nil { + logger.Error(err) + continue + } + } else if len(route.Services) == 1 { + fullName, serversLB, err := cb.nameAndService(ctx, ingressRoute.Namespace, route.Services[0].LoadBalancerSpec) + if err != nil { + logger.Error(err) + continue + } + + if serversLB != nil { + conf.Services[serviceName] = serversLB + } else { + serviceName = fullName + } + } + + conf.Routers[normalized] = &dynamic.Router{ Middlewares: mds, Priority: route.Priority, EntryPoints: ingressRoute.Spec.EntryPoints, @@ -132,7 +135,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli tlsOptionsName := ingressRoute.Spec.TLS.Options.Name // Is a Kubernetes CRD reference, (i.e. not a cross-provider reference) ns := ingressRoute.Spec.TLS.Options.Namespace - if !strings.Contains(tlsOptionsName, "@") { + if !strings.Contains(tlsOptionsName, providerNamespaceSeparator) { if len(ns) == 0 { ns = ingressRoute.Namespace } @@ -145,7 +148,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli tlsConf.Options = tlsOptionsName } - conf.Routers[serviceName].TLS = tlsConf + conf.Routers[normalized].TLS = tlsConf } } } @@ -153,120 +156,277 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli return conf } -func createLoadBalancerServerHTTP(client Client, namespace string, service v1alpha1.Service) (*dynamic.Service, error) { - servers, err := loadServers(client, namespace, service) +type configBuilder struct { + client Client +} + +// buildTraefikService creates the configuration for the traefik service defined in tService, +// and adds it to the given conf map. +func (c configBuilder) buildTraefikService(ctx context.Context, tService *v1alpha1.TraefikService, conf map[string]*dynamic.Service) error { + id := provider.Normalize(makeID(tService.Namespace, tService.Name)) + + if tService.Spec.Weighted != nil { + return c.buildServicesLB(ctx, tService.Namespace, tService.Spec, id, conf) + } else if tService.Spec.Mirroring != nil { + return c.buildMirroring(ctx, tService, id, conf) + } + + return errors.New("unspecified service type") +} + +// buildServicesLB creates the configuration for the load-balancer of services named id, and defined in tService. +// It adds it to the given conf map. +func (c configBuilder) buildServicesLB(ctx context.Context, namespace string, tService v1alpha1.ServiceSpec, id string, conf map[string]*dynamic.Service) error { + var wrrServices []dynamic.WRRService + + for _, service := range tService.Weighted.Services { + fullName, k8sService, err := c.nameAndService(ctx, namespace, service.LoadBalancerSpec) + if err != nil { + return err + } + + if k8sService != nil { + conf[fullName] = k8sService + } + + weight := service.Weight + if weight == nil { + weight = func(i int) *int { return &i }(1) + } + + wrrServices = append(wrrServices, dynamic.WRRService{ + Name: fullName, + Weight: weight, + }) + } + + conf[id] = &dynamic.Service{ + Weighted: &dynamic.WeightedRoundRobin{ + Services: wrrServices, + Sticky: tService.Weighted.Sticky, + }, + } + return nil +} + +// buildMirroring creates the configuration for the mirroring service named id, and defined by tService. +// It adds it to the given conf map. +func (c configBuilder) buildMirroring(ctx context.Context, tService *v1alpha1.TraefikService, id string, conf map[string]*dynamic.Service) error { + fullNameMain, k8sService, err := c.nameAndService(ctx, tService.Namespace, tService.Spec.Mirroring.LoadBalancerSpec) + if err != nil { + return err + } + + if k8sService != nil { + conf[fullNameMain] = k8sService + } + + var mirrorServices []dynamic.MirrorService + for _, mirror := range tService.Spec.Mirroring.Mirrors { + mirroredName, k8sService, err := c.nameAndService(ctx, tService.Namespace, mirror.LoadBalancerSpec) + if err != nil { + return err + } + + if k8sService != nil { + conf[mirroredName] = k8sService + } + + mirrorServices = append(mirrorServices, dynamic.MirrorService{ + Name: mirroredName, + Percent: mirror.Percent, + }) + } + + conf[id] = &dynamic.Service{ + Mirroring: &dynamic.Mirroring{ + Service: fullNameMain, + Mirrors: mirrorServices, + }, + } + + return nil +} + +// buildServersLB creates the configuration for the load-balancer of servers defined by svc. +func (c configBuilder) buildServersLB(namespace string, svc v1alpha1.LoadBalancerSpec) (*dynamic.Service, error) { + servers, err := c.loadServers(namespace, svc) if err != nil { return nil, err } - // TODO: support other strategies. lb := &dynamic.ServersLoadBalancer{} lb.SetDefaults() - lb.Servers = servers - lb.PassHostHeader = service.PassHostHeader + conf := svc + lb.PassHostHeader = conf.PassHostHeader if lb.PassHostHeader == nil { passHostHeader := true lb.PassHostHeader = &passHostHeader } - lb.ResponseForwarding = service.ResponseForwarding + lb.ResponseForwarding = conf.ResponseForwarding - return &dynamic.Service{ - LoadBalancer: lb, - }, nil + lb.Sticky = svc.Sticky + + return &dynamic.Service{LoadBalancer: lb}, nil } -func loadServers(client Client, namespace string, svc v1alpha1.Service) ([]dynamic.Server, error) { +func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBalancerSpec) ([]dynamic.Server, error) { strategy := svc.Strategy if strategy == "" { - strategy = "RoundRobin" + strategy = roundRobinStrategy } - if strategy != "RoundRobin" { - return nil, fmt.Errorf("load balancing strategy %v is not supported", strategy) + if strategy != roundRobinStrategy { + return nil, fmt.Errorf("load balancing strategy %s is not supported", strategy) } - service, exists, err := client.GetService(namespace, svc.Name) + namespace := namespaceOrFallback(svc, fallbackNamespace) + + // If the service uses explicitly the provider suffix + sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+providerName) + service, exists, err := c.client.GetService(namespace, sanitizedName) if err != nil { return nil, err } - if !exists { - return nil, fmt.Errorf("service not found %s/%s", namespace, svc.Name) + return nil, fmt.Errorf("kubernetes service not found: %s/%s", namespace, sanitizedName) } + confPort := svc.Port var portSpec *corev1.ServicePort for _, p := range service.Spec.Ports { - if svc.Port == p.Port { + if confPort == p.Port { portSpec = &p break } } - if portSpec == nil { return nil, errors.New("service port not found") } var servers []dynamic.Server if service.Spec.Type == corev1.ServiceTypeExternalName { - protocol := "http" - if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { - protocol = "https" + return append(servers, dynamic.Server{ + URL: fmt.Sprintf("http://%s:%d", service.Spec.ExternalName, portSpec.Port), + }), nil + } + + endpoints, endpointsExists, endpointsErr := c.client.GetEndpoints(namespace, sanitizedName) + if endpointsErr != nil { + return nil, endpointsErr + } + if !endpointsExists { + return nil, fmt.Errorf("endpoints not found for %s/%s", namespace, sanitizedName) + } + if len(endpoints.Subsets) == 0 { + return nil, fmt.Errorf("subset not found for %s/%s", namespace, sanitizedName) + } + + var port int32 + for _, subset := range endpoints.Subsets { + for _, p := range subset.Ports { + if portSpec.Name == p.Name { + port = p.Port + break + } } - servers = append(servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, portSpec.Port), - }) - } else { - endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) - if endpointsErr != nil { - return nil, endpointsErr + if port == 0 { + return nil, fmt.Errorf("cannot define a port for %s/%s", namespace, sanitizedName) } - if !endpointsExists { - return nil, errors.New("endpoints not found") + protocol := httpProtocol + scheme := svc.Scheme + switch scheme { + case httpProtocol, httpsProtocol, "h2c": + protocol = scheme + case "": + if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, httpsProtocol) { + protocol = httpsProtocol + } + default: + return nil, fmt.Errorf("invalid scheme %q specified", scheme) } - 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 portSpec.Name == p.Name { - port = p.Port - break - } - } - - if port == 0 { - return nil, errors.New("cannot define a port") - } - - protocol := "http" - switch svc.Scheme { - case "http", "https", "h2c": - protocol = svc.Scheme - case "": - if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { - protocol = "https" - } - default: - return nil, fmt.Errorf("invalid scheme %q specified", svc.Scheme) - } - - for _, addr := range subset.Addresses { - servers = append(servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port), - }) - } + for _, addr := range subset.Addresses { + servers = append(servers, dynamic.Server{ + URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port), + }) } } return servers, nil } +// nameAndService returns the name that should be used for the svc service in the generated config. +// In addition, if the service is a Kubernetes one, +// it generates and returns the configuration part for such a service, +// so that the caller can add it to the global config map. +func (c configBuilder) nameAndService(ctx context.Context, namespaceService string, service v1alpha1.LoadBalancerSpec) (string, *dynamic.Service, error) { + svcCtx := log.With(ctx, log.Str(log.ServiceName, service.Name)) + + namespace := namespaceOrFallback(service, namespaceService) + + switch { + case service.Kind == "" || service.Kind == "Service": + serversLB, err := c.buildServersLB(namespace, service) + if err != nil { + return "", nil, err + } + + fullName := fullServiceName(svcCtx, namespace, service.Name, service.Port) + + return fullName, serversLB, nil + case service.Kind == "TraefikService": + return fullServiceName(svcCtx, namespace, service.Name, 0), nil, nil + default: + return "", nil, fmt.Errorf("unsupported service kind %s", service.Kind) + } +} + +func splitSvcNameProvider(name string) (string, string) { + parts := strings.Split(name, providerNamespaceSeparator) + + svc := strings.Join(parts[:len(parts)-1], providerNamespaceSeparator) + pvd := parts[len(parts)-1] + + return svc, pvd +} + +func fullServiceName(ctx context.Context, namespace, serviceName string, port int32) string { + if port != 0 { + return provider.Normalize(fmt.Sprintf("%s-%s-%d", namespace, serviceName, port)) + } + + if !strings.Contains(serviceName, providerNamespaceSeparator) { + return provider.Normalize(fmt.Sprintf("%s-%s", namespace, serviceName)) + } + + name, pName := splitSvcNameProvider(serviceName) + if pName == providerName { + return provider.Normalize(fmt.Sprintf("%s-%s", namespace, name)) + } + + // At this point, if namespace == "default", we do not know whether it had been intentionally set as such, + // or if we're simply hitting the value set by default. + // But as it is most likely very much the latter, + // and we do not want to systematically log spam users in that case, + // we skip logging whenever the namespace is "default". + if namespace != "default" { + log.FromContext(ctx).Warnf("namespace %q is ignored in cross-provider context", namespace) + } + + return provider.Normalize(name) + providerNamespaceSeparator + pName +} + +func namespaceOrFallback(lb v1alpha1.LoadBalancerSpec, fallback string) string { + if lb.Namespace != "" { + return lb.Namespace + } + return fallback +} + func getTLSHTTP(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { if ingressRoute.Spec.TLS == nil { return nil diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 142a99576..7444dd353 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -719,16 +719,16 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"foo"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -756,9 +756,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test2.route-23c7f4c450289ee29016": { + "default-test2-route-23c7f4c450289ee29016": { EntryPoints: []string{"web"}, - Service: "default-test2.route-23c7f4c450289ee29016", + Service: "default-test2-route-23c7f4c450289ee29016", Rule: "Host(`foo.com`) && PathPrefix(`/tobestripped`)", Priority: 12, Middlewares: []string{"default-stripprefix", "foo-addprefix"}, @@ -777,7 +777,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, Services: map[string]*dynamic.Service{ - "default-test2.route-23c7f4c450289ee29016": { + "default-test2-route-23c7f4c450289ee29016": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -806,9 +806,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test2.route-23c7f4c450289ee29016": { + "default-test2-route-23c7f4c450289ee29016": { EntryPoints: []string{"web"}, - Service: "default-test2.route-23c7f4c450289ee29016", + Service: "default-test2-route-23c7f4c450289ee29016", Rule: "Host(`foo.com`) && PathPrefix(`/tobestripped`)", Priority: 12, Middlewares: []string{"default-stripprefix", "foo-addprefix", "basicauth@file", "redirect@file"}, @@ -827,7 +827,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, Services: map[string]*dynamic.Service{ - "default-test2.route-23c7f4c450289ee29016": { + "default-test2-route-23c7f4c450289ee29016": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -854,22 +854,22 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Priority: 14, }, - "default-test.route-77c62dfe9517144aeeaa": { + "default-test-route-77c62dfe9517144aeeaa": { EntryPoints: []string{"web"}, - Service: "default-test.route-77c62dfe9517144aeeaa", + Service: "default-test-route-77c62dfe9517144aeeaa", Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", Priority: 12, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -882,7 +882,7 @@ func TestLoadIngressRoutes(t *testing.T) { PassHostHeader: Bool(true), }, }, - "default-test.route-77c62dfe9517144aeeaa": { + "default-test-route-77c62dfe9517144aeeaa": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -911,30 +911,30 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-77c62dfe9517144aeeaa": { + "default-test-route-77c62dfe9517144aeeaa": { EntryPoints: []string{"web"}, - Service: "default-test.route-77c62dfe9517144aeeaa", + Service: "default-test-route-77c62dfe9517144aeeaa", Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", Priority: 12, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-77c62dfe9517144aeeaa": { + "default-test-route-77c62dfe9517144aeeaa": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { - Name: "default-test.route-77c62dfe9517144aeeaa-whoami-80", + Name: "default-whoami-80", Weight: func(i int) *int { return &i }(1), }, { - Name: "default-test.route-77c62dfe9517144aeeaa-whoami2-8080", + Name: "default-whoami2-8080", Weight: func(i int) *int { return &i }(1), }, }, }, }, - "default-test.route-77c62dfe9517144aeeaa-whoami-80": { + "default-whoami-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -947,7 +947,667 @@ func TestLoadIngressRoutes(t *testing.T) { PassHostHeader: Bool(true), }, }, - "default-test.route-77c62dfe9517144aeeaa-whoami2-8080": { + "default-whoami2-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one kube service (== servers lb) in a services wrr", + paths: []string{"with_services_lb0.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default-wrr1", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-wrr1": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami5-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "traefik service without ingress route", + paths: []string{"with_services_only.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-wrr1": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami5-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "One ingress Route with two different services, each with two services, balancing servers nested", + paths: []string{"with_services_lb1.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default-test-route-77c62dfe9517144aeeaa", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test-route-77c62dfe9517144aeeaa": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-wrr1", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-wrr2", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-wrr1": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami4-80", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami4-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "default-whoami5-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "default-wrr2": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami6-80", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-whoami7-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami6-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.5:80", + }, + { + URL: "http://10.10.0.6:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "default-whoami7-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.7:8080", + }, + { + URL: "http://10.10.0.8:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "one wrr and one kube service (== servers lb) in a wrr", + paths: []string{"with_services_lb2.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default-wrr1", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-wrr1": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-wrr2", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-wrr2": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami5-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "services lb, servers lb, and mirror service, all in a wrr", + paths: []string{"with_services_lb3.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default-wrr1", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-wrr1": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-wrr2", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-mirror1", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-wrr2": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-mirror1": { + Mirroring: &dynamic.Mirroring{ + Service: "default-whoami5-8080", + Mirrors: []dynamic.MirrorService{ + {Name: "default-whoami4-8080", Percent: 50}, + }, + }, + }, + "default-whoami4-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + { + URL: "http://10.10.0.2:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "default-whoami5-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "services lb, servers lb, and mirror service, all in a wrr with different namespaces", + paths: []string{"with_namespaces.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default-test-route-77c62dfe9517144aeeaa", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test-route-77c62dfe9517144aeeaa": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "baz-whoami6-8080", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "foo-wrr1", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "foo-mirror2", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "foo-mirror3", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "foo-mirror4", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "baz-whoami6-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.5:8080", + }, + { + URL: "http://10.10.0.6:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "foo-wrr1": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "foo-whoami4-8080", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "baz-whoami6-8080", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "foo-mirror1", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "bar-wrr2", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "foo-whoami4-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + { + URL: "http://10.10.0.2:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "foo-mirror1": { + Mirroring: &dynamic.Mirroring{ + Service: "foo-whoami5-8080", + Mirrors: []dynamic.MirrorService{ + {Name: "foo-whoami4-8080"}, + {Name: "baz-whoami6-8080"}, + {Name: "bar-mirrored"}, + }, + }, + }, + "foo-whoami5-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "bar-mirrored": { + Mirroring: &dynamic.Mirroring{ + Service: "baz-whoami6-8080", + Mirrors: []dynamic.MirrorService{ + {Name: "foo-whoami4-8080", Percent: 50}, + }, + }, + }, + "foo-mirror2": { + Mirroring: &dynamic.Mirroring{ + Service: "foo-whoami5-8080", + Mirrors: []dynamic.MirrorService{ + {Name: "foo-whoami4-8080"}, + {Name: "baz-whoami6-8080"}, + {Name: "bar-mirrored"}, + {Name: "foo-wrr1"}, + }, + }, + }, + "foo-mirror3": { + Mirroring: &dynamic.Mirroring{ + Service: "foo-wrr1", + Mirrors: []dynamic.MirrorService{ + {Name: "foo-whoami4-8080"}, + {Name: "baz-whoami6-8080"}, + {Name: "bar-mirrored"}, + {Name: "foo-wrr1"}, + }, + }, + }, + "bar-wrr2": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "foo-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "foo-mirror4": { + Mirroring: &dynamic.Mirroring{ + Service: "foo-wrr1", + Mirrors: []dynamic.MirrorService{ + {Name: "foo-whoami4-8080"}, + {Name: "baz-whoami6-8080"}, + {Name: "bar-mirrored"}, + {Name: "foo-wrr1"}, + }, + }, + }, + }, + }, + }, + }, + { + desc: "one kube service (== servers lb) in a mirroring", + paths: []string{"with_mirroring.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default-mirror1", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-mirror1": { + Mirroring: &dynamic.Mirroring{ + Service: "default-whoami5-8080", + Mirrors: []dynamic.MirrorService{ + {Name: "default-whoami4-8080", Percent: 50}, + }, + }, + }, + "default-whoami4-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + { + URL: "http://10.10.0.2:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "default-whoami5-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "weighted services in a mirroring", + paths: []string{"with_mirroring2.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default-mirror1", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-mirror1": { + Mirroring: &dynamic.Mirroring{ + Service: "default-wrr1", + Mirrors: []dynamic.MirrorService{ + {Name: "default-wrr2", Percent: 30}, + }, + }, + }, + "default-wrr1": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami4-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-wrr2": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami5-8080", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami4-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + { + URL: "http://10.10.0.2:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + "default-whoami5-8080": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -975,30 +1635,30 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-77c62dfe9517144aeeaa": { + "default-test-route-77c62dfe9517144aeeaa": { EntryPoints: []string{"web"}, - Service: "default-test.route-77c62dfe9517144aeeaa", + Service: "default-test-route-77c62dfe9517144aeeaa", Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", Priority: 12, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-77c62dfe9517144aeeaa": { + "default-test-route-77c62dfe9517144aeeaa": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { - Name: "default-test.route-77c62dfe9517144aeeaa-whoami-80", + Name: "default-whoami-80", Weight: Int(10), }, { - Name: "default-test.route-77c62dfe9517144aeeaa-whoami2-8080", + Name: "default-whoami2-8080", Weight: Int(0), }, }, }, }, - "default-test.route-77c62dfe9517144aeeaa-whoami-80": { + "default-whoami-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1011,7 +1671,7 @@ func TestLoadIngressRoutes(t *testing.T) { PassHostHeader: Bool(true), }, }, - "default-test.route-77c62dfe9517144aeeaa-whoami2-8080": { + "default-whoami2-8080": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1113,9 +1773,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &dynamic.RouterTLSConfig{}, @@ -1123,7 +1783,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1169,9 +1829,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &dynamic.RouterTLSConfig{ @@ -1181,7 +1841,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1227,9 +1887,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &dynamic.RouterTLSConfig{ @@ -1239,7 +1899,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1284,9 +1944,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &dynamic.RouterTLSConfig{ @@ -1296,7 +1956,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1330,9 +1990,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &dynamic.RouterTLSConfig{ @@ -1342,7 +2002,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1376,9 +2036,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &dynamic.RouterTLSConfig{ @@ -1388,7 +2048,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1416,9 +2076,9 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &dynamic.RouterTLSConfig{}, @@ -1426,7 +2086,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1454,16 +2114,16 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"foo"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1491,16 +2151,16 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"foo"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { @@ -1602,16 +2262,16 @@ func TestLoadIngressRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { EntryPoints: []string{"foo"}, - Service: "default-test.route-6b204d94623b3df4370c", + Service: "default-test-route-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-test.route-6b204d94623b3df4370c": { + "default-test-route-6b204d94623b3df4370c": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ { diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go index a1f5d325c..3cb2705c1 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go @@ -1,6 +1,9 @@ package v1alpha1 import ( + "errors" + "fmt" + "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,13 +25,14 @@ type Route struct { Middlewares []MiddlewareRef `json:"middlewares"` } -// TLS contains the TLS certificates configuration of the routes. To enable -// Let's Encrypt, use an empty TLS struct, e.g. in YAML: +// TLS contains the TLS certificates configuration of the routes. +// To enable Let's Encrypt, use an empty TLS struct, +// e.g. in YAML: // -// tls: {} # inline format +// tls: {} # inline format // -// tls: -// secretName: # block format +// tls: +// secretName: # block format type TLS struct { // SecretName is the name of the referenced Kubernetes Secret to specify the // certificate details. @@ -45,16 +49,58 @@ type TLSOptionRef struct { Namespace string `json:"namespace"` } -// Service defines an upstream to proxy traffic. -type Service struct { - Name string `json:"name"` +// LoadBalancerSpec can reference either a Kubernetes Service object (a load-balancer of servers), +// or a TraefikService object (a traefik load-balancer of services). +type LoadBalancerSpec struct { + // Name is a reference to a Kubernetes Service object (for a load-balancer of servers), + // or to a TraefikService object (service load-balancer, mirroring, etc). + // The differentiation between the two is specified in the Kind field. + Name string `json:"name"` + Kind string `json:"kind"` + Namespace string `json:"namespace"` + Sticky *dynamic.Sticky `json:"sticky,omitempty"` + + // Port and all the fields below are related to a servers load-balancer, + // and therefore should only be specified when Name references a Kubernetes Service. Port int32 `json:"port"` Scheme string `json:"scheme,omitempty"` HealthCheck *HealthCheck `json:"healthCheck,omitempty"` Strategy string `json:"strategy,omitempty"` PassHostHeader *bool `json:"passHostHeader,omitempty"` ResponseForwarding *dynamic.ResponseForwarding `json:"responseForwarding,omitempty"` - Weight *int `json:"weight,omitempty"` + + // Weight should only be specified when Name references a TraefikService object + // (and to be precise, one that embeds a Weighted Round Robin). + Weight *int `json:"weight,omitempty"` +} + +// IsServersLB reports whether lb is a load-balancer of servers +// (as opposed to a traefik load-balancer of services). +func (lb LoadBalancerSpec) IsServersLB() (bool, error) { + if lb.Name == "" { + return false, errors.New("missing Name field in service") + } + if lb.Kind == "" || lb.Kind == "Service" { + return true, nil + } + if lb.Kind != "TraefikService" { + return false, fmt.Errorf("invalid kind value: %v", lb.Kind) + } + if lb.Port != 0 || + lb.Scheme != "" || + lb.HealthCheck != nil || + lb.Strategy != "" || + lb.PassHostHeader != nil || + lb.ResponseForwarding != nil || + lb.Sticky != nil { + return false, fmt.Errorf("service of kind %v is incompatible with Kubernetes Service related fields", lb.Kind) + } + return false, nil +} + +// Service defines an upstream to proxy traffic. +type Service struct { + LoadBalancerSpec } // MiddlewareRef is a ref to the Middleware resources. diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go index 72c9a7d0e..50dc7a49e 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go @@ -18,13 +18,14 @@ type RouteTCP struct { Services []ServiceTCP `json:"services,omitempty"` } -// TLSTCP contains the TLS certificates configuration of the routes. To enable -// Let's Encrypt, use an empty TLS struct, e.g. in YAML: +// TLSTCP contains the TLS certificates configuration of the routes. +// To enable Let's Encrypt, use an empty TLS struct, +// e.g. in YAML: // -// tls: {} # inline format +// tls: {} # inline format // -// tls: -// secretName: # block format +// tls: +// secretName: # block format type TLSTCP struct { // SecretName is the name of the referenced Kubernetes Secret to specify the // certificate details. diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/register.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/register.go index 15278474a..ef3341e8c 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/register.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/register.go @@ -41,6 +41,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &MiddlewareList{}, &TLSOption{}, &TLSOptionList{}, + &TraefikService{}, + &TraefikServiceList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/service.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/service.go new file mode 100644 index 000000000..da29b6b5c --- /dev/null +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/service.go @@ -0,0 +1,64 @@ +package v1alpha1 + +import ( + "github.com/containous/traefik/v2/pkg/config/dynamic" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TraefikService is the specification for a service (that an IngressRoute refers +// to) that is usually not a terminal service (i.e. not a pod of servers), as +// opposed to a Kubernetes Service. That is to say, it usually refers to other +// (children) services, which themselves can be TraefikServices or Services. +type TraefikService struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + Spec ServiceSpec `json:"spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TraefikServiceList is a list of TraefikService resources. +type TraefikServiceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []TraefikService `json:"items"` +} + +// +k8s:deepcopy-gen=true + +// ServiceSpec defines whether a TraefikService is a load-balancer of services or a +// mirroring service. +type ServiceSpec struct { + Weighted *WeightedRoundRobin `json:"weighted,omitempty"` + Mirroring *Mirroring `json:"mirroring,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// Mirroring defines a mirroring service, which is composed of a main +// load-balancer, and a list of mirrors. +type Mirroring struct { + LoadBalancerSpec + Mirrors []MirrorService `json:"mirrors,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// MirrorService defines one of the mirrors of a Mirroring service. +type MirrorService struct { + LoadBalancerSpec + Percent int `json:"percent,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// WeightedRoundRobin defines a load-balancer of services. +type WeightedRoundRobin struct { + Services []Service `json:"services,omitempty"` + Sticky *dynamic.Sticky `json:"sticky,omitempty"` +} diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 4b1e0918a..9bce44c99 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -381,6 +381,47 @@ func (in *IngressRouteTCPSpec) DeepCopy() *IngressRouteTCPSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) { + *out = *in + if in.Sticky != nil { + in, out := &in.Sticky, &out.Sticky + *out = new(dynamic.Sticky) + (*in).DeepCopyInto(*out) + } + if in.HealthCheck != nil { + in, out := &in.HealthCheck, &out.HealthCheck + *out = new(HealthCheck) + (*in).DeepCopyInto(*out) + } + if in.PassHostHeader != nil { + in, out := &in.PassHostHeader, &out.PassHostHeader + *out = new(bool) + **out = **in + } + if in.ResponseForwarding != nil { + in, out := &in.ResponseForwarding, &out.ResponseForwarding + *out = new(dynamic.ResponseForwarding) + **out = **in + } + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerSpec. +func (in *LoadBalancerSpec) DeepCopy() *LoadBalancerSpec { + if in == nil { + return nil + } + out := new(LoadBalancerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Middleware) DeepCopyInto(out *Middleware) { *out = *in @@ -578,6 +619,47 @@ func (in *MiddlewareSpec) DeepCopy() *MiddlewareSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MirrorService) DeepCopyInto(out *MirrorService) { + *out = *in + in.LoadBalancerSpec.DeepCopyInto(&out.LoadBalancerSpec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MirrorService. +func (in *MirrorService) DeepCopy() *MirrorService { + if in == nil { + return nil + } + out := new(MirrorService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Mirroring) DeepCopyInto(out *Mirroring) { + *out = *in + in.LoadBalancerSpec.DeepCopyInto(&out.LoadBalancerSpec) + if in.Mirrors != nil { + in, out := &in.Mirrors, &out.Mirrors + *out = make([]MirrorService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mirroring. +func (in *Mirroring) DeepCopy() *Mirroring { + if in == nil { + return nil + } + out := new(Mirroring) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Route) DeepCopyInto(out *Route) { *out = *in @@ -632,26 +714,7 @@ func (in *RouteTCP) DeepCopy() *RouteTCP { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Service) DeepCopyInto(out *Service) { *out = *in - if in.HealthCheck != nil { - in, out := &in.HealthCheck, &out.HealthCheck - *out = new(HealthCheck) - (*in).DeepCopyInto(*out) - } - if in.PassHostHeader != nil { - in, out := &in.PassHostHeader, &out.PassHostHeader - *out = new(bool) - **out = **in - } - if in.ResponseForwarding != nil { - in, out := &in.ResponseForwarding, &out.ResponseForwarding - *out = new(dynamic.ResponseForwarding) - **out = **in - } - if in.Weight != nil { - in, out := &in.Weight, &out.Weight - *out = new(int) - **out = **in - } + in.LoadBalancerSpec.DeepCopyInto(&out.LoadBalancerSpec) return } @@ -665,6 +728,32 @@ func (in *Service) DeepCopy() *Service { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) { + *out = *in + if in.Weighted != nil { + in, out := &in.Weighted, &out.Weighted + *out = new(WeightedRoundRobin) + (*in).DeepCopyInto(*out) + } + if in.Mirroring != nil { + in, out := &in.Mirroring, &out.Mirroring + *out = new(Mirroring) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec. +func (in *ServiceSpec) DeepCopy() *ServiceSpec { + if in == nil { + return nil + } + out := new(ServiceSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceTCP) DeepCopyInto(out *ServiceTCP) { *out = *in @@ -865,3 +954,91 @@ func (in *TLSTCP) DeepCopy() *TLSTCP { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TraefikService) DeepCopyInto(out *TraefikService) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TraefikService. +func (in *TraefikService) DeepCopy() *TraefikService { + if in == nil { + return nil + } + out := new(TraefikService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TraefikService) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TraefikServiceList) DeepCopyInto(out *TraefikServiceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TraefikService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TraefikServiceList. +func (in *TraefikServiceList) DeepCopy() *TraefikServiceList { + if in == nil { + return nil + } + out := new(TraefikServiceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TraefikServiceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WeightedRoundRobin) DeepCopyInto(out *WeightedRoundRobin) { + *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]Service, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Sticky != nil { + in, out := &in.Sticky, &out.Sticky + *out = new(dynamic.Sticky) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightedRoundRobin. +func (in *WeightedRoundRobin) DeepCopy() *WeightedRoundRobin { + if in == nil { + return nil + } + out := new(WeightedRoundRobin) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/provider/kubernetes/k8s/parser.go b/pkg/provider/kubernetes/k8s/parser.go index b154872fe..3b7be6e51 100644 --- a/pkg/provider/kubernetes/k8s/parser.go +++ b/pkg/provider/kubernetes/k8s/parser.go @@ -12,7 +12,7 @@ import ( // MustParseYaml parses a YAML to objects. func MustParseYaml(content []byte) []runtime.Object { - acceptedK8sTypes := regexp.MustCompile(`(Deployment|Endpoints|Service|Ingress|IngressRoute|Middleware|Secret|TLSOption)`) + acceptedK8sTypes := regexp.MustCompile(`^(Deployment|Endpoints|Service|Ingress|IngressRoute|IngressRouteTCP|Middleware|Secret|TLSOption|TraefikService)$`) files := strings.Split(string(content), "---") retVal := make([]runtime.Object, 0, len(files))