From c4df78b4b9b46c239d6da17201d82f4ed50fca14 Mon Sep 17 00:00:00 2001 From: mpl Date: Tue, 11 Jun 2019 15:12:04 +0200 Subject: [PATCH] Add support for TCP (in kubernetes CRD) Co-authored-by: Jean-Baptiste Doumenjou --- docs/content/providers/crd_ingress_route.yml | 15 + docs/content/providers/kubernetes-crd.md | 18 +- .../dynamic-configuration/kubernetes-crd.yml | 37 +- docs/content/user-guides/crd-acme/01-crd.yml | 23 + .../user-guides/crd-acme/04-ingressroutes.yml | 4 +- integration/fixtures/k8s/01-crd.yml | 15 + integration/fixtures/k8s/02-services.yml | 43 ++ .../fixtures/k8s/05-ingressroutetcp.yml | 14 + integration/fixtures/k8s_crd.toml | 3 + integration/k8s_test.go | 131 ++++- integration/testdata/rawdata-crd.json | 102 ++++ integration/testdata/rawdata-ingress.json | 31 ++ pkg/config/zz_generated.deepcopy.go | 15 + pkg/provider/aggregator/aggregator.go | 1 + pkg/provider/kubernetes/crd/client.go | 16 + .../kubernetes/crd/client_mock_test.go | 14 +- .../kubernetes/crd/fixtures/tcp/services.yml | 88 ++++ .../kubernetes/crd/fixtures/tcp/simple.yml | 15 + .../crd/fixtures/tcp/with_bad_host_rule.yml | 15 + .../crd/fixtures/tcp/with_no_rule_value.yml | 15 + .../kubernetes/crd/fixtures/tcp/with_tls.yml | 29 ++ .../crd/fixtures/tcp/with_tls_acme.yml | 18 + .../crd/fixtures/tcp/with_tls_passthrough.yml | 30 ++ .../crd/fixtures/tcp/with_two_rules.yml | 19 + .../crd/fixtures/tcp/with_two_services.yml | 18 + .../kubernetes/crd/fixtures/with_tls_acme.yml | 3 +- .../v1alpha1/fake/fake_ingressroutetcp.go | 136 ++++++ .../v1alpha1/fake/fake_traefik_client.go | 4 + .../traefik/v1alpha1/generated_expansion.go | 2 + .../typed/traefik/v1alpha1/ingressroutetcp.go | 165 +++++++ .../typed/traefik/v1alpha1/traefik_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../traefik/v1alpha1/ingressroutetcp.go | 97 ++++ .../traefik/v1alpha1/interface.go | 7 + .../traefik/v1alpha1/expansion_generated.go | 8 + .../traefik/v1alpha1/ingressroutetcp.go | 102 ++++ pkg/provider/kubernetes/crd/kubernetes.go | 237 +++++++-- .../kubernetes/crd/kubernetes_test.go | 448 ++++++++++++++++-- .../crd/traefik/v1alpha1/ingressroute.go | 7 +- .../crd/traefik/v1alpha1/ingressroutetcp.go | 58 +++ .../crd/traefik/v1alpha1/register.go | 2 + .../traefik/v1alpha1/zz_generated.deepcopy.go | 146 ++++++ pkg/provider/kubernetes/ingress/kubernetes.go | 2 +- pkg/server/service/tcp/service.go | 3 +- 44 files changed, 2070 insertions(+), 93 deletions(-) create mode 100644 integration/fixtures/k8s/05-ingressroutetcp.yml create mode 100644 integration/testdata/rawdata-crd.json create mode 100644 integration/testdata/rawdata-ingress.json create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/services.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/simple.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_bad_host_rule.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_no_rule_value.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_tls.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_acme.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_passthrough.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_two_rules.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_two_services.yml create mode 100644 pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_ingressroutetcp.go create mode 100644 pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/ingressroutetcp.go create mode 100644 pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/ingressroutetcp.go create mode 100644 pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/ingressroutetcp.go create mode 100644 pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go diff --git a/docs/content/providers/crd_ingress_route.yml b/docs/content/providers/crd_ingress_route.yml index 0bcfd3568..b40f22527 100644 --- a/docs/content/providers/crd_ingress_route.yml +++ b/docs/content/providers/crd_ingress_route.yml @@ -11,3 +11,18 @@ spec: plural: ingressroutes singular: ingressroute scope: Namespaced + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: ingressroutetcps.traefik.containo.us + +spec: + group: traefik.containo.us + version: v1alpha1 + names: + kind: IngressRouteTCP + plural: ingressroutetcps + singular: ingressroutetcp + scope: Namespaced diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index 9d0a3e06c..19745dfc1 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -145,7 +145,7 @@ If you're in a hurry, maybe you'd rather go through the [dynamic](../reference/d --8<-- "content/providers/crd_ingress_route.yml" ``` -That `IngressRoute` kind can then be used to define an `IngressRoute` object, such as: +That `IngressRoute` kind can then be used to define an `IngressRoute` object, such as in: ```yaml apiVersion: traefik.containo.us/v1alpha1 @@ -170,6 +170,22 @@ spec: services: - name: whoami port: 80 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: ingressroutetcpfoo.crd + +spec: + entryPoints: + - footcp + routes: + # Match is the rule corresponding to an underlying router. + - match: HostSNI(`*`) + services: + - name: whoamitcp + port: 8080 ``` ### Middleware diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd.yml index 44162db8a..2850d669c 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd.yml @@ -26,6 +26,21 @@ spec: singular: middleware scope: Namespaced +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: ingressroutetcps.traefik.containo.us + +spec: + group: traefik.containo.us + version: v1alpha1 + names: + kind: IngressRouteTCP + plural: ingressroutetcps + singular: ingressroutetcp + scope: Namespaced + --- apiVersion: traefik.containo.us/v1alpha1 kind: IngressRoute @@ -67,5 +82,25 @@ spec: middlewares: - name: stripprefix - name: addprefix + # use an empty tls object for TLS with Let's Encrypt tls: - secretName: supersecret \ No newline at end of file + secretName: supersecret + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: ingressroutetcp.crd + namespace: default + +spec: + entryPoints: + - footcp + routes: + - match: HostSNI(`bar.com`) + services: + - name: whoamitcp + port: 8080 + tls: + secretName: foosecret + passthrough: false diff --git a/docs/content/user-guides/crd-acme/01-crd.yml b/docs/content/user-guides/crd-acme/01-crd.yml index 45ace3f90..8914ccac1 100644 --- a/docs/content/user-guides/crd-acme/01-crd.yml +++ b/docs/content/user-guides/crd-acme/01-crd.yml @@ -12,6 +12,21 @@ spec: singular: ingressroute scope: Namespaced +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: ingressroutetcps.traefik.containo.us + +spec: + group: traefik.containo.us + version: v1alpha1 + names: + kind: IngressRouteTCP + plural: ingressroutetcps + singular: ingressroutetcp + scope: Namespaced + --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition @@ -74,6 +89,14 @@ rules: - get - list - watch + - apiGroups: + - traefik.containo.us + resources: + - ingressroutetcps + verbs: + - get + - list + - watch --- kind: ClusterRoleBinding diff --git a/docs/content/user-guides/crd-acme/04-ingressroutes.yml b/docs/content/user-guides/crd-acme/04-ingressroutes.yml index a2d6ab0ce..04baed826 100644 --- a/docs/content/user-guides/crd-acme/04-ingressroutes.yml +++ b/docs/content/user-guides/crd-acme/04-ingressroutes.yml @@ -26,5 +26,5 @@ spec: services: - name: whoami port: 80 - tls: - secretName: "" + # Please note the use of an empty TLS object to enable TLS with Let's Encrypt. + tls: {} diff --git a/integration/fixtures/k8s/01-crd.yml b/integration/fixtures/k8s/01-crd.yml index e8f78d03e..f3992350d 100644 --- a/integration/fixtures/k8s/01-crd.yml +++ b/integration/fixtures/k8s/01-crd.yml @@ -26,3 +26,18 @@ spec: plural: middlewares singular: middleware scope: Namespaced + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: ingressroutetcps.traefik.containo.us + +spec: + group: traefik.containo.us + version: v1alpha1 + names: + kind: IngressRouteTCP + plural: ingressroutetcps + singular: ingressroutetcp + scope: Namespaced diff --git a/integration/fixtures/k8s/02-services.yml b/integration/fixtures/k8s/02-services.yml index 20b80f97d..13002c402 100644 --- a/integration/fixtures/k8s/02-services.yml +++ b/integration/fixtures/k8s/02-services.yml @@ -39,3 +39,46 @@ spec: selector: app: containous task: whoami + +--- +kind: Deployment +apiVersion: extensions/v1beta1 +metadata: + name: whoamitcp + namespace: default + labels: + app: containous + name: whoamitcp + +spec: + replicas: 2 + selector: + matchLabels: + app: containous + task: whoamitcp + template: + metadata: + labels: + app: containous + task: whoamitcp + spec: + containers: + - name: containouswhoamitcp + image: containous/whoamitcp + ports: + - containerPort: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoamitcp + namespace: default + +spec: + ports: + - name: footcp + port: 8080 + selector: + app: containous + task: whoamitcp diff --git a/integration/fixtures/k8s/05-ingressroutetcp.yml b/integration/fixtures/k8s/05-ingressroutetcp.yml new file mode 100644 index 000000000..ec84bd21b --- /dev/null +++ b/integration/fixtures/k8s/05-ingressroutetcp.yml @@ -0,0 +1,14 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test3.crd + namespace: default + +spec: + entryPoints: + - footcp + routes: + - match: HostSNI(`*`) + services: + - name: whoamitcp + port: 8080 diff --git a/integration/fixtures/k8s_crd.toml b/integration/fixtures/k8s_crd.toml index 787403409..d34f6a03a 100644 --- a/integration/fixtures/k8s_crd.toml +++ b/integration/fixtures/k8s_crd.toml @@ -8,6 +8,9 @@ level = "DEBUG" [entryPoints] [entryPoints.web] address = ":8000" + [entryPoints.footcp] + address = ":8093" + [api] diff --git a/integration/k8s_test.go b/integration/k8s_test.go index 07ca7e7be..4bb6cb02d 100644 --- a/integration/k8s_test.go +++ b/integration/k8s_test.go @@ -1,16 +1,28 @@ package integration import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" "net/http" "os" "path/filepath" + "regexp" "time" "github.com/containous/traefik/integration/try" + "github.com/containous/traefik/pkg/api" "github.com/go-check/check" + "github.com/pmezard/go-difflib/difflib" checker "github.com/vdemeester/shakers" ) +var updateExpected = flag.Bool("update_expected", false, "Update expected files in testdata") + // K8sSuite type K8sSuite struct{ BaseSuite } @@ -48,7 +60,7 @@ func (s *K8sSuite) TearDownSuite(c *check.C) { } } -func (s *K8sSuite) TestIngressSimple(c *check.C) { +func (s *K8sSuite) TestIngressConfiguration(c *check.C) { cmd, display := s.traefikCmd(withConfigFile("fixtures/k8s_default.toml")) defer display(c) @@ -56,11 +68,10 @@ func (s *K8sSuite) TestIngressSimple(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("Host(`whoami.test`)")) - c.Assert(err, checker.IsNil) + testConfiguration(c, "testdata/rawdata-ingress.json") } -func (s *K8sSuite) TestCRDSimple(c *check.C) { +func (s *K8sSuite) TestCRDConfiguration(c *check.C) { cmd, display := s.traefikCmd(withConfigFile("fixtures/k8s_crd.toml")) defer display(c) @@ -68,15 +79,105 @@ func (s *K8sSuite) TestCRDSimple(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 60*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("Host(`foo.com`)")) - c.Assert(err, checker.IsNil) - - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("PathPrefix(`/tobestripped`)")) - c.Assert(err, checker.IsNil) - - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("default/stripprefix")) - c.Assert(err, checker.IsNil) - - err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains("stripprefix")) - c.Assert(err, checker.IsNil) + testConfiguration(c, "testdata/rawdata-crd.json") +} + +func testConfiguration(c *check.C, path string) { + expectedJSON := filepath.FromSlash(path) + + if *updateExpected { + fi, err := os.Create(expectedJSON) + c.Assert(err, checker.IsNil) + err = fi.Close() + c.Assert(err, checker.IsNil) + } + + var buf bytes.Buffer + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 20*time.Second, try.StatusCodeIs(http.StatusOK), matchesConfig(expectedJSON, &buf)) + + if !*updateExpected { + if err != nil { + c.Error(err) + } + return + } + + if err != nil { + c.Logf("In file update mode, got expected error: %v", err) + } + + var rtRepr api.RunTimeRepresentation + err = json.Unmarshal(buf.Bytes(), &rtRepr) + c.Assert(err, checker.IsNil) + + newJSON, err := json.MarshalIndent(rtRepr, "", "\t") + c.Assert(err, checker.IsNil) + + err = ioutil.WriteFile(expectedJSON, newJSON, 0644) + c.Assert(err, checker.IsNil) + c.Errorf("We do not want a passing test in file update mode") +} + +func matchesConfig(wantConfig string, buf *bytes.Buffer) try.ResponseCondition { + return func(res *http.Response) error { + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %s", err) + } + + if err := res.Body.Close(); err != nil { + return err + } + + var obtained api.RunTimeRepresentation + err = json.Unmarshal(body, &obtained) + if err != nil { + return err + } + + if buf != nil { + buf.Reset() + if _, err := io.Copy(buf, bytes.NewReader(body)); err != nil { + return err + } + } + + got, err := json.MarshalIndent(obtained, "", "\t") + if err != nil { + return err + } + + expected, err := ioutil.ReadFile(wantConfig) + if err != nil { + return err + } + + // The pods IPs are dynamic, so we cannot predict them, + // which is why we have to ignore them in the comparison. + rxURL := regexp.MustCompile(`"(url|address)":\s+(".*")`) + sanitizedExpected := rxURL.ReplaceAll(expected, []byte(`"$1": "XXXX"`)) + sanitizedGot := rxURL.ReplaceAll(got, []byte(`"$1": "XXXX"`)) + + rxServerStatus := regexp.MustCompile(`"http://.*?":\s+(".*")`) + sanitizedExpected = rxServerStatus.ReplaceAll(sanitizedExpected, []byte(`"http://XXXX": $1`)) + sanitizedGot = rxServerStatus.ReplaceAll(sanitizedGot, []byte(`"http://XXXX": $1`)) + + if bytes.Equal(sanitizedExpected, sanitizedGot) { + return nil + } + + diff := difflib.UnifiedDiff{ + FromFile: "Expected", + A: difflib.SplitLines(string(sanitizedExpected)), + ToFile: "Got", + B: difflib.SplitLines(string(sanitizedGot)), + Context: 3, + } + + text, err := difflib.GetUnifiedDiffString(diff) + if err != nil { + return err + } + return errors.New(text) + } } diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json new file mode 100644 index 000000000..fe53a081f --- /dev/null +++ b/integration/testdata/rawdata-crd.json @@ -0,0 +1,102 @@ +{ + "routers": { + "kubernetescrd.default/test-crd-6b204d94623b3df4370c": { + "entryPoints": [ + "web" + ], + "service": "default/test-crd-6b204d94623b3df4370c", + "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/bar`)", + "priority": 12 + }, + "kubernetescrd.default/test2-crd-23c7f4c450289ee29016": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "default/stripprefix" + ], + "service": "default/test2-crd-23c7f4c450289ee29016", + "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/tobestripped`)" + } + }, + "middlewares": { + "kubernetescrd.default/stripprefix": { + "stripPrefix": { + "prefixes": [ + "/tobestripped" + ] + }, + "usedBy": [ + "kubernetescrd.default/test2-crd-23c7f4c450289ee29016" + ] + } + }, + "services": { + "kubernetescrd.default/test-crd-6b204d94623b3df4370c": { + "loadbalancer": { + "servers": [ + { + "url": "http://10.42.0.4:80" + }, + { + "url": "http://10.42.0.5:80" + } + ], + "passHostHeader": true + }, + "usedBy": [ + "kubernetescrd.default/test-crd-6b204d94623b3df4370c" + ], + "serverStatus": { + "http://10.42.0.4:80": "UP", + "http://10.42.0.5:80": "UP" + } + }, + "kubernetescrd.default/test2-crd-23c7f4c450289ee29016": { + "loadbalancer": { + "servers": [ + { + "url": "http://10.42.0.4:80" + }, + { + "url": "http://10.42.0.5:80" + } + ], + "passHostHeader": true + }, + "usedBy": [ + "kubernetescrd.default/test2-crd-23c7f4c450289ee29016" + ], + "serverStatus": { + "http://10.42.0.4:80": "UP", + "http://10.42.0.5:80": "UP" + } + } + }, + "tcpRouters": { + "kubernetescrd.default/test3-crd-673acf455cb2dab0b43a": { + "entryPoints": [ + "footcp" + ], + "service": "default/test3-crd-673acf455cb2dab0b43a", + "rule": "HostSNI(`*`)" + } + }, + "tcpServices": { + "kubernetescrd.default/test3-crd-673acf455cb2dab0b43a": { + "loadbalancer": { + "servers": [ + { + "address": "10.42.0.2:8080" + }, + { + "address": "10.42.0.3:8080" + } + ] + }, + "usedBy": [ + "kubernetescrd.default/test3-crd-673acf455cb2dab0b43a" + ] + } + } +} \ No newline at end of file diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json new file mode 100644 index 000000000..e55c94f80 --- /dev/null +++ b/integration/testdata/rawdata-ingress.json @@ -0,0 +1,31 @@ +{ + "routers": { + "kubernetes.whoami-test/whoami": { + "entryPoints": null, + "service": "default/whoami/http", + "rule": "Host(`whoami.test`) \u0026\u0026 PathPrefix(`/whoami`)" + } + }, + "services": { + "kubernetes.default/whoami/http": { + "loadbalancer": { + "servers": [ + { + "url": "http://10.42.0.2:80" + }, + { + "url": "http://10.42.0.5:80" + } + ], + "passHostHeader": true + }, + "usedBy": [ + "kubernetes.whoami-test/whoami" + ], + "serverStatus": { + "http://10.42.0.2:80": "UP", + "http://10.42.0.5:80": "UP" + } + } + } +} \ No newline at end of file diff --git a/pkg/config/zz_generated.deepcopy.go b/pkg/config/zz_generated.deepcopy.go index 52e66d476..2cf48762f 100644 --- a/pkg/config/zz_generated.deepcopy.go +++ b/pkg/config/zz_generated.deepcopy.go @@ -266,6 +266,21 @@ func (in *Headers) DeepCopyInto(out *Headers) { (*out)[key] = val } } + if in.AccessControlAllowHeaders != nil { + in, out := &in.AccessControlAllowHeaders, &out.AccessControlAllowHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AccessControlAllowMethods != nil { + in, out := &in.AccessControlAllowMethods, &out.AccessControlAllowMethods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AccessControlExposeHeaders != nil { + in, out := &in.AccessControlExposeHeaders, &out.AccessControlExposeHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.AllowedHosts != nil { in, out := &in.AllowedHosts, &out.AllowedHosts *out = make([]string, len(*in)) diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index 47b942787..016c9a632 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -44,6 +44,7 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { if conf.KubernetesCRD != nil { p.quietAddProvider(conf.KubernetesCRD) } + if conf.Rancher != nil { p.quietAddProvider(conf.Rancher) } diff --git a/pkg/provider/kubernetes/crd/client.go b/pkg/provider/kubernetes/crd/client.go index 948453189..3e5f393e7 100644 --- a/pkg/provider/kubernetes/crd/client.go +++ b/pkg/provider/kubernetes/crd/client.go @@ -48,6 +48,7 @@ type Client interface { WatchAll(namespaces k8s.Namespaces, stopCh <-chan struct{}) (<-chan interface{}, error) GetIngressRoutes() []*v1alpha1.IngressRoute + GetIngressRouteTCPs() []*v1alpha1.IngressRouteTCP GetMiddlewares() []*v1alpha1.Middleware GetIngresses() []*extensionsv1beta1.Ingress @@ -157,6 +158,7 @@ func (c *clientWrapper) WatchAll(namespaces k8s.Namespaces, stopCh <-chan struct factoryCrd := externalversions.NewSharedInformerFactoryWithOptions(c.csCrd, resyncPeriod, externalversions.WithNamespace(ns)) factoryCrd.Traefik().V1alpha1().IngressRoutes().Informer().AddEventHandler(eventHandler) factoryCrd.Traefik().V1alpha1().Middlewares().Informer().AddEventHandler(eventHandler) + factoryCrd.Traefik().V1alpha1().IngressRouteTCPs().Informer().AddEventHandler(eventHandler) factoryKube := informers.NewFilteredSharedInformerFactory(c.csKube, resyncPeriod, ns, nil) factoryKube.Extensions().V1beta1().Ingresses().Informer().AddEventHandler(eventHandler) @@ -212,6 +214,20 @@ func (c *clientWrapper) GetIngressRoutes() []*v1alpha1.IngressRoute { return result } +func (c *clientWrapper) GetIngressRouteTCPs() []*v1alpha1.IngressRouteTCP { + var result []*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) + } + result = append(result, ings...) + } + + return result +} + func (c *clientWrapper) GetMiddlewares() []*v1alpha1.Middleware { var result []*v1alpha1.Middleware diff --git a/pkg/provider/kubernetes/crd/client_mock_test.go b/pkg/provider/kubernetes/crd/client_mock_test.go index 87b70b73e..625975702 100644 --- a/pkg/provider/kubernetes/crd/client_mock_test.go +++ b/pkg/provider/kubernetes/crd/client_mock_test.go @@ -3,6 +3,7 @@ package crd import ( "fmt" "io/ioutil" + "path/filepath" "github.com/containous/traefik/pkg/provider/kubernetes/crd/traefik/v1alpha1" "github.com/containous/traefik/pkg/provider/kubernetes/k8s" @@ -33,8 +34,9 @@ type clientMock struct { apiEndpointsError error apiIngressStatusError error - ingressRoutes []*v1alpha1.IngressRoute - middlewares []*v1alpha1.Middleware + ingressRoutes []*v1alpha1.IngressRoute + ingressRouteTCPs []*v1alpha1.IngressRouteTCP + middlewares []*v1alpha1.Middleware watchChan chan interface{} } @@ -43,7 +45,7 @@ func newClientMock(paths ...string) clientMock { var c clientMock for _, path := range paths { - yamlContent, err := ioutil.ReadFile("./fixtures/" + path) + yamlContent, err := ioutil.ReadFile(filepath.FromSlash("./fixtures/" + path)) if err != nil { panic(err) } @@ -57,6 +59,8 @@ func newClientMock(paths ...string) clientMock { c.endpoints = append(c.endpoints, o) case *v1alpha1.IngressRoute: c.ingressRoutes = append(c.ingressRoutes, o) + case *v1alpha1.IngressRouteTCP: + c.ingressRouteTCPs = append(c.ingressRouteTCPs, o) case *v1alpha1.Middleware: c.middlewares = append(c.middlewares, o) case *v1beta12.Ingress: @@ -76,6 +80,10 @@ func (c clientMock) GetIngressRoutes() []*v1alpha1.IngressRoute { return c.ingressRoutes } +func (c clientMock) GetIngressRouteTCPs() []*v1alpha1.IngressRouteTCP { + return c.ingressRouteTCPs +} + func (c clientMock) GetMiddlewares() []*v1alpha1.Middleware { return c.middlewares } diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml new file mode 100644 index 000000000..5f6615f2d --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml @@ -0,0 +1,88 @@ +apiVersion: v1 +kind: Service +metadata: + name: whoamitcp + namespace: default + +spec: + ports: + - name: myapp + port: 8000 + selector: + app: containous + task: whoamitcp + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoamitcp + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.1 + - ip: 10.10.0.2 + ports: + - name: myapp + port: 8000 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoamitcp2 + namespace: default + +spec: + ports: + - name: myapp2 + port: 8080 + selector: + app: containous + task: whoamitcp2 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoamitcp2 + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.3 + - ip: 10.10.0.4 + ports: + - name: myapp2 + port: 8080 + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoamitcptls + namespace: default + +spec: + ports: + - name: web-secure + port: 443 + selector: + app: containous + task: whoamitcptls2 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: whoamitcptls + namespace: default + +subsets: + - addresses: + - ip: 10.10.0.5 + - ip: 10.10.0.6 + ports: + - name: web-secure + port: 443 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/simple.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/simple.yml new file mode 100644 index 000000000..db1e0ca87 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/simple.yml @@ -0,0 +1,15 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.crd + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: whoamitcp + port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_bad_host_rule.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_bad_host_rule.yml new file mode 100644 index 000000000..b525c0619 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_bad_host_rule.yml @@ -0,0 +1,15 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.crd + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: HostSNI(`foo.com"0"`) + services: + - name: whoamitcp + port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_no_rule_value.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_no_rule_value.yml new file mode 100644 index 000000000..8a51c2e6f --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_no_rule_value.yml @@ -0,0 +1,15 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.crd + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: "" + services: + - name: whoamitcp + port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls.yml new file mode 100644 index 000000000..33237f2bc --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls.yml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Secret +metadata: + name: supersecret + namespace: default + +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.crd + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: whoamitcp + port: 8000 + + tls: + secretName: supersecret diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_acme.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_acme.yml new file mode 100644 index 000000000..43fcf5938 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_acme.yml @@ -0,0 +1,18 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.crd + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: whoamitcp + port: 8000 + + tls: + secretName: diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_passthrough.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_passthrough.yml new file mode 100644 index 000000000..5c440a4a4 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_passthrough.yml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Secret +metadata: + name: supersecret + namespace: default + +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.crd + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: whoamitcp + port: 8000 + + tls: + secretName: supersecret + passthrough: true diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_two_rules.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_two_rules.yml new file mode 100644 index 000000000..f07c7b25f --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_two_rules.yml @@ -0,0 +1,19 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.crd + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: whoamitcp + port: 8000 + - match: HostSNI(`bar.com`) + services: + - name: whoamitcp + port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_two_services.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_two_services.yml new file mode 100644 index 000000000..0cbdfa513 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_two_services.yml @@ -0,0 +1,18 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.crd + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: whoamitcp + port: 8000 + - name: whoamitcp2 + port: 8080 + diff --git a/pkg/provider/kubernetes/crd/fixtures/with_tls_acme.yml b/pkg/provider/kubernetes/crd/fixtures/with_tls_acme.yml index dde9941b8..3b289e1bc 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_tls_acme.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_tls_acme.yml @@ -16,5 +16,4 @@ spec: - name: whoami port: 80 - tls: - secretName: + tls: {} diff --git a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_ingressroutetcp.go b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_ingressroutetcp.go new file mode 100644 index 000000000..9ec7b4f08 --- /dev/null +++ b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/fake/fake_ingressroutetcp.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/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" +) + +// FakeIngressRouteTCPs implements IngressRouteTCPInterface +type FakeIngressRouteTCPs struct { + Fake *FakeTraefikV1alpha1 + ns string +} + +var ingressroutetcpsResource = schema.GroupVersionResource{Group: "traefik.containo.us", Version: "v1alpha1", Resource: "ingressroutetcps"} + +var ingressroutetcpsKind = schema.GroupVersionKind{Group: "traefik.containo.us", Version: "v1alpha1", Kind: "IngressRouteTCP"} + +// Get takes name of the ingressRouteTCP, and returns the corresponding ingressRouteTCP object, and an error if there is any. +func (c *FakeIngressRouteTCPs) Get(name string, options v1.GetOptions) (result *v1alpha1.IngressRouteTCP, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(ingressroutetcpsResource, c.ns, name), &v1alpha1.IngressRouteTCP{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.IngressRouteTCP), err +} + +// List takes label and field selectors, and returns the list of IngressRouteTCPs that match those selectors. +func (c *FakeIngressRouteTCPs) List(opts v1.ListOptions) (result *v1alpha1.IngressRouteTCPList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(ingressroutetcpsResource, ingressroutetcpsKind, c.ns, opts), &v1alpha1.IngressRouteTCPList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.IngressRouteTCPList{ListMeta: obj.(*v1alpha1.IngressRouteTCPList).ListMeta} + for _, item := range obj.(*v1alpha1.IngressRouteTCPList).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 ingressRouteTCPs. +func (c *FakeIngressRouteTCPs) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(ingressroutetcpsResource, c.ns, opts)) + +} + +// Create takes the representation of a ingressRouteTCP and creates it. Returns the server's representation of the ingressRouteTCP, and an error, if there is any. +func (c *FakeIngressRouteTCPs) Create(ingressRouteTCP *v1alpha1.IngressRouteTCP) (result *v1alpha1.IngressRouteTCP, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(ingressroutetcpsResource, c.ns, ingressRouteTCP), &v1alpha1.IngressRouteTCP{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.IngressRouteTCP), err +} + +// Update takes the representation of a ingressRouteTCP and updates it. Returns the server's representation of the ingressRouteTCP, and an error, if there is any. +func (c *FakeIngressRouteTCPs) Update(ingressRouteTCP *v1alpha1.IngressRouteTCP) (result *v1alpha1.IngressRouteTCP, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(ingressroutetcpsResource, c.ns, ingressRouteTCP), &v1alpha1.IngressRouteTCP{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.IngressRouteTCP), err +} + +// Delete takes name of the ingressRouteTCP and deletes it. Returns an error if one occurs. +func (c *FakeIngressRouteTCPs) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(ingressroutetcpsResource, c.ns, name), &v1alpha1.IngressRouteTCP{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeIngressRouteTCPs) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(ingressroutetcpsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.IngressRouteTCPList{}) + return err +} + +// Patch applies the patch and returns the patched ingressRouteTCP. +func (c *FakeIngressRouteTCPs) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.IngressRouteTCP, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(ingressroutetcpsResource, c.ns, name, data, subresources...), &v1alpha1.IngressRouteTCP{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.IngressRouteTCP), err +} 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 c2ada49ce..2a4094a68 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 @@ -40,6 +40,10 @@ func (c *FakeTraefikV1alpha1) IngressRoutes(namespace string) v1alpha1.IngressRo return &FakeIngressRoutes{c, namespace} } +func (c *FakeTraefikV1alpha1) IngressRouteTCPs(namespace string) v1alpha1.IngressRouteTCPInterface { + return &FakeIngressRouteTCPs{c, namespace} +} + func (c *FakeTraefikV1alpha1) Middlewares(namespace string) v1alpha1.MiddlewareInterface { return &FakeMiddlewares{c, namespace} } 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 cf8c2cfa7..2a9108930 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 @@ -28,4 +28,6 @@ package v1alpha1 type IngressRouteExpansion interface{} +type IngressRouteTCPExpansion interface{} + type MiddlewareExpansion interface{} diff --git a/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/ingressroutetcp.go new file mode 100644 index 000000000..47a9fe59e --- /dev/null +++ b/pkg/provider/kubernetes/crd/generated/clientset/versioned/typed/traefik/v1alpha1/ingressroutetcp.go @@ -0,0 +1,165 @@ +/* +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 ( + scheme "github.com/containous/traefik/pkg/provider/kubernetes/crd/generated/clientset/versioned/scheme" + v1alpha1 "github.com/containous/traefik/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" +) + +// IngressRouteTCPsGetter has a method to return a IngressRouteTCPInterface. +// A group's client should implement this interface. +type IngressRouteTCPsGetter interface { + IngressRouteTCPs(namespace string) IngressRouteTCPInterface +} + +// IngressRouteTCPInterface has methods to work with IngressRouteTCP resources. +type IngressRouteTCPInterface interface { + Create(*v1alpha1.IngressRouteTCP) (*v1alpha1.IngressRouteTCP, error) + Update(*v1alpha1.IngressRouteTCP) (*v1alpha1.IngressRouteTCP, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.IngressRouteTCP, error) + List(opts v1.ListOptions) (*v1alpha1.IngressRouteTCPList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.IngressRouteTCP, err error) + IngressRouteTCPExpansion +} + +// ingressRouteTCPs implements IngressRouteTCPInterface +type ingressRouteTCPs struct { + client rest.Interface + ns string +} + +// newIngressRouteTCPs returns a IngressRouteTCPs +func newIngressRouteTCPs(c *TraefikV1alpha1Client, namespace string) *ingressRouteTCPs { + return &ingressRouteTCPs{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the ingressRouteTCP, and returns the corresponding ingressRouteTCP object, and an error if there is any. +func (c *ingressRouteTCPs) Get(name string, options v1.GetOptions) (result *v1alpha1.IngressRouteTCP, err error) { + result = &v1alpha1.IngressRouteTCP{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ingressroutetcps"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of IngressRouteTCPs that match those selectors. +func (c *ingressRouteTCPs) List(opts v1.ListOptions) (result *v1alpha1.IngressRouteTCPList, err error) { + result = &v1alpha1.IngressRouteTCPList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ingressroutetcps"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested ingressRouteTCPs. +func (c *ingressRouteTCPs) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("ingressroutetcps"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a ingressRouteTCP and creates it. Returns the server's representation of the ingressRouteTCP, and an error, if there is any. +func (c *ingressRouteTCPs) Create(ingressRouteTCP *v1alpha1.IngressRouteTCP) (result *v1alpha1.IngressRouteTCP, err error) { + result = &v1alpha1.IngressRouteTCP{} + err = c.client.Post(). + Namespace(c.ns). + Resource("ingressroutetcps"). + Body(ingressRouteTCP). + Do(). + Into(result) + return +} + +// Update takes the representation of a ingressRouteTCP and updates it. Returns the server's representation of the ingressRouteTCP, and an error, if there is any. +func (c *ingressRouteTCPs) Update(ingressRouteTCP *v1alpha1.IngressRouteTCP) (result *v1alpha1.IngressRouteTCP, err error) { + result = &v1alpha1.IngressRouteTCP{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ingressroutetcps"). + Name(ingressRouteTCP.Name). + Body(ingressRouteTCP). + Do(). + Into(result) + return +} + +// Delete takes name of the ingressRouteTCP and deletes it. Returns an error if one occurs. +func (c *ingressRouteTCPs) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("ingressroutetcps"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *ingressRouteTCPs) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("ingressroutetcps"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched ingressRouteTCP. +func (c *ingressRouteTCPs) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.IngressRouteTCP, err error) { + result = &v1alpha1.IngressRouteTCP{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("ingressroutetcps"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} 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 5b69b0557..e1b0f47b5 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 @@ -36,6 +36,7 @@ import ( type TraefikV1alpha1Interface interface { RESTClient() rest.Interface IngressRoutesGetter + IngressRouteTCPsGetter MiddlewaresGetter } @@ -48,6 +49,10 @@ func (c *TraefikV1alpha1Client) IngressRoutes(namespace string) IngressRouteInte return newIngressRoutes(c, namespace) } +func (c *TraefikV1alpha1Client) IngressRouteTCPs(namespace string) IngressRouteTCPInterface { + return newIngressRouteTCPs(c, namespace) +} + func (c *TraefikV1alpha1Client) Middlewares(namespace string) MiddlewareInterface { return newMiddlewares(c, namespace) } diff --git a/pkg/provider/kubernetes/crd/generated/informers/externalversions/generic.go b/pkg/provider/kubernetes/crd/generated/informers/externalversions/generic.go index 8970ae03f..fcd191a3c 100644 --- a/pkg/provider/kubernetes/crd/generated/informers/externalversions/generic.go +++ b/pkg/provider/kubernetes/crd/generated/informers/externalversions/generic.go @@ -63,6 +63,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=traefik.containo.us, Version=v1alpha1 case v1alpha1.SchemeGroupVersion.WithResource("ingressroutes"): return &genericInformer{resource: resource.GroupResource(), informer: f.Traefik().V1alpha1().IngressRoutes().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("ingressroutetcps"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Traefik().V1alpha1().IngressRouteTCPs().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("middlewares"): return &genericInformer{resource: resource.GroupResource(), informer: f.Traefik().V1alpha1().Middlewares().Informer()}, nil diff --git a/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/ingressroutetcp.go new file mode 100644 index 000000000..f4b328d01 --- /dev/null +++ b/pkg/provider/kubernetes/crd/generated/informers/externalversions/traefik/v1alpha1/ingressroutetcp.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/pkg/provider/kubernetes/crd/generated/clientset/versioned" + internalinterfaces "github.com/containous/traefik/pkg/provider/kubernetes/crd/generated/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/containous/traefik/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1" + traefikv1alpha1 "github.com/containous/traefik/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" +) + +// IngressRouteTCPInformer provides access to a shared informer and lister for +// IngressRouteTCPs. +type IngressRouteTCPInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.IngressRouteTCPLister +} + +type ingressRouteTCPInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewIngressRouteTCPInformer constructs a new informer for IngressRouteTCP 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 NewIngressRouteTCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredIngressRouteTCPInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredIngressRouteTCPInformer constructs a new informer for IngressRouteTCP 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 NewFilteredIngressRouteTCPInformer(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().IngressRouteTCPs(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.TraefikV1alpha1().IngressRouteTCPs(namespace).Watch(options) + }, + }, + &traefikv1alpha1.IngressRouteTCP{}, + resyncPeriod, + indexers, + ) +} + +func (f *ingressRouteTCPInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredIngressRouteTCPInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *ingressRouteTCPInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&traefikv1alpha1.IngressRouteTCP{}, f.defaultInformer) +} + +func (f *ingressRouteTCPInformer) Lister() v1alpha1.IngressRouteTCPLister { + return v1alpha1.NewIngressRouteTCPLister(f.Informer().GetIndexer()) +} 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 c8a2beef4..80c6a617d 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 @@ -34,6 +34,8 @@ import ( type Interface interface { // IngressRoutes returns a IngressRouteInformer. IngressRoutes() IngressRouteInformer + // IngressRouteTCPs returns a IngressRouteTCPInformer. + IngressRouteTCPs() IngressRouteTCPInformer // Middlewares returns a MiddlewareInformer. Middlewares() MiddlewareInformer } @@ -54,6 +56,11 @@ func (v *version) IngressRoutes() IngressRouteInformer { return &ingressRouteInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// IngressRouteTCPs returns a IngressRouteTCPInformer. +func (v *version) IngressRouteTCPs() IngressRouteTCPInformer { + return &ingressRouteTCPInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Middlewares returns a MiddlewareInformer. func (v *version) Middlewares() MiddlewareInformer { return &middlewareInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 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 c0c85f6f3..1463476eb 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 @@ -34,6 +34,14 @@ type IngressRouteListerExpansion interface{} // IngressRouteNamespaceLister. type IngressRouteNamespaceListerExpansion interface{} +// IngressRouteTCPListerExpansion allows custom methods to be added to +// IngressRouteTCPLister. +type IngressRouteTCPListerExpansion interface{} + +// IngressRouteTCPNamespaceListerExpansion allows custom methods to be added to +// IngressRouteTCPNamespaceLister. +type IngressRouteTCPNamespaceListerExpansion interface{} + // MiddlewareListerExpansion allows custom methods to be added to // MiddlewareLister. type MiddlewareListerExpansion interface{} diff --git a/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/ingressroutetcp.go new file mode 100644 index 000000000..c3c758ba1 --- /dev/null +++ b/pkg/provider/kubernetes/crd/generated/listers/traefik/v1alpha1/ingressroutetcp.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/pkg/provider/kubernetes/crd/traefik/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// IngressRouteTCPLister helps list IngressRouteTCPs. +type IngressRouteTCPLister interface { + // List lists all IngressRouteTCPs in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.IngressRouteTCP, err error) + // IngressRouteTCPs returns an object that can list and get IngressRouteTCPs. + IngressRouteTCPs(namespace string) IngressRouteTCPNamespaceLister + IngressRouteTCPListerExpansion +} + +// ingressRouteTCPLister implements the IngressRouteTCPLister interface. +type ingressRouteTCPLister struct { + indexer cache.Indexer +} + +// NewIngressRouteTCPLister returns a new IngressRouteTCPLister. +func NewIngressRouteTCPLister(indexer cache.Indexer) IngressRouteTCPLister { + return &ingressRouteTCPLister{indexer: indexer} +} + +// List lists all IngressRouteTCPs in the indexer. +func (s *ingressRouteTCPLister) List(selector labels.Selector) (ret []*v1alpha1.IngressRouteTCP, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.IngressRouteTCP)) + }) + return ret, err +} + +// IngressRouteTCPs returns an object that can list and get IngressRouteTCPs. +func (s *ingressRouteTCPLister) IngressRouteTCPs(namespace string) IngressRouteTCPNamespaceLister { + return ingressRouteTCPNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// IngressRouteTCPNamespaceLister helps list and get IngressRouteTCPs. +type IngressRouteTCPNamespaceLister interface { + // List lists all IngressRouteTCPs in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.IngressRouteTCP, err error) + // Get retrieves the IngressRouteTCP from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.IngressRouteTCP, error) + IngressRouteTCPNamespaceListerExpansion +} + +// ingressRouteTCPNamespaceLister implements the IngressRouteTCPNamespaceLister +// interface. +type ingressRouteTCPNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all IngressRouteTCPs in the indexer for a given namespace. +func (s ingressRouteTCPNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.IngressRouteTCP, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.IngressRouteTCP)) + }) + return ret, err +} + +// Get retrieves the IngressRouteTCP from the indexer for a given namespace and name. +func (s ingressRouteTCPNamespaceLister) Get(name string) (*v1alpha1.IngressRouteTCP, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("ingressroutetcp"), name) + } + return obj.(*v1alpha1.IngressRouteTCP), nil +} diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index d735450c4..9e9d8bd08 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -151,6 +151,71 @@ func checkStringQuoteValidity(value string) error { return err } +func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]config.TCPServer, error) { + service, exists, err := client.GetService(namespace, svc.Name) + if err != nil { + return nil, err + } + + if !exists { + return nil, errors.New("service not found") + } + + var portSpec *corev1.ServicePort + for _, p := range service.Spec.Ports { + if svc.Port == p.Port { + portSpec = &p + break + } + } + + if portSpec == nil { + return nil, errors.New("service port not found") + } + + var servers []config.TCPServer + if service.Spec.Type == corev1.ServiceTypeExternalName { + servers = append(servers, config.TCPServer{ + Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, portSpec.Port), + }) + } else { + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) + if endpointsErr != nil { + return nil, endpointsErr + } + + if !endpointsExists { + return nil, errors.New("endpoints not found") + } + + if len(endpoints.Subsets) == 0 { + return nil, errors.New("subset not found") + } + + var port int32 + for _, subset := range endpoints.Subsets { + for _, p := range subset.Ports { + if portSpec.Name == p.Name { + port = p.Port + break + } + } + + if port == 0 { + return nil, errors.New("cannot define a port") + } + + for _, addr := range subset.Addresses { + servers = append(servers, config.TCPServer{ + Address: fmt.Sprintf("%s:%d", addr.IP, port), + }) + } + } + } + + return servers, nil +} + func loadServers(client Client, namespace string, svc v1alpha1.Service) ([]config.Server, error) { strategy := svc.Strategy if strategy == "" { @@ -169,18 +234,15 @@ func loadServers(client Client, namespace string, svc v1alpha1.Service) ([]confi return nil, errors.New("service not found") } - var portSpec corev1.ServicePort - var match bool - // TODO: support name ports? do we actually care? + var portSpec *corev1.ServicePort for _, p := range service.Spec.Ports { if svc.Port == p.Port { - portSpec = p - match = true + portSpec = &p break } } - if !match { + if portSpec == nil { return nil, errors.New("service port not found") } @@ -233,14 +295,16 @@ func loadServers(client Client, namespace string, svc v1alpha1.Service) ([]confi } func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Client) *config.Configuration { - conf := &config.Configuration{ HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{}, Middlewares: map[string]*config.Middleware{}, Services: map[string]*config.Service{}, }, - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, } tlsConfigs := make(map[string]*tls.Configuration) @@ -252,7 +316,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl continue } - err := getTLS(ctx, ingressRoute, client, tlsConfigs) + err := getTLSHTTP(ctx, ingressRoute, client, tlsConfigs) if err != nil { logger.Errorf("Error configuring TLS: %v", err) } @@ -306,13 +370,11 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl mds = append(mds, makeID(ns, mi.Name)) } - h := sha256.New() - _, err = h.Write([]byte(route.Match)) + key, err := makeServiceKey(route.Match, ingressName) if err != nil { logger.Error(err) continue } - key := fmt.Sprintf("%s-%.10x", ingressName, h.Sum(nil)) serviceName := makeID(ingressRoute.Namespace, key) @@ -336,19 +398,103 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl } } - conf.TLS = getTLSConfig(tlsConfigs) - for _, middleware := range client.GetMiddlewares() { conf.HTTP.Middlewares[makeID(middleware.Namespace, middleware.Name)] = &middleware.Spec } + for _, ingressRouteTCP := range client.GetIngressRouteTCPs() { + logger := log.FromContext(log.With(ctx, log.Str("ingress", ingressRouteTCP.Name), log.Str("namespace", ingressRouteTCP.Namespace))) + + if !shouldProcessIngress(p.IngressClass, ingressRouteTCP.Annotations[annotationKubernetesIngressClass]) { + continue + } + + if ingressRouteTCP.Spec.TLS != nil && !ingressRouteTCP.Spec.TLS.Passthrough { + err := getTLSTCP(ctx, ingressRouteTCP, client, tlsConfigs) + if err != nil { + logger.Errorf("Error configuring TLS: %v", err) + } + } + + ingressName := ingressRouteTCP.Name + if len(ingressName) == 0 { + ingressName = ingressRouteTCP.GenerateName + } + + for _, route := range ingressRouteTCP.Spec.Routes { + if len(route.Match) == 0 { + logger.Errorf("Empty match rule") + continue + } + + if err := checkStringQuoteValidity(route.Match); err != nil { + logger.Errorf("Invalid syntax for match rule: %s", route.Match) + continue + } + + var allServers []config.TCPServer + for _, service := range route.Services { + servers, err := loadTCPServers(client, ingressRouteTCP.Namespace, service) + if err != nil { + logger. + WithField("serviceName", service.Name). + WithField("servicePort", service.Port). + Errorf("Cannot create service: %v", err) + continue + } + + allServers = append(allServers, servers...) + } + + key, e := makeServiceKey(route.Match, ingressName) + if e != nil { + logger.Error(e) + continue + } + + serviceName := makeID(ingressRouteTCP.Namespace, key) + conf.TCP.Routers[serviceName] = &config.TCPRouter{ + EntryPoints: ingressRouteTCP.Spec.EntryPoints, + Rule: route.Match, + Service: serviceName, + } + + if ingressRouteTCP.Spec.TLS != nil { + conf.TCP.Routers[serviceName].TLS = &config.RouterTCPTLSConfig{ + Passthrough: ingressRouteTCP.Spec.TLS.Passthrough, + } + } + + conf.TCP.Services[serviceName] = &config.TCPService{ + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: allServers, + }, + } + } + } + + conf.TLS = getTLSConfig(tlsConfigs) + return conf } +func makeServiceKey(rule, ingressName string) (string, error) { + h := sha256.New() + if _, err := h.Write([]byte(rule)); err != nil { + return "", err + } + + ingressName = strings.ReplaceAll(ingressName, ".", "-") + key := fmt.Sprintf("%s-%.10x", ingressName, h.Sum(nil)) + + return key, nil +} + func makeID(namespace, name string) string { if namespace == "" { return name } + return namespace + "/" + name } @@ -357,7 +503,7 @@ func shouldProcessIngress(ingressClass string, ingressClassAnnotation string) bo (len(ingressClass) == 0 && ingressClassAnnotation == traefikDefaultIngressClass) } -func getTLS(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.Configuration) error { +func getTLSHTTP(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.Configuration) error { if ingressRoute.Spec.TLS == nil { return nil } @@ -368,30 +514,61 @@ func getTLS(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient configKey := ingressRoute.Namespace + "/" + ingressRoute.Spec.TLS.SecretName if _, tlsExists := tlsConfigs[configKey]; !tlsExists { - secret, exists, err := k8sClient.GetSecret(ingressRoute.Namespace, ingressRoute.Spec.TLS.SecretName) - if err != nil { - return fmt.Errorf("failed to fetch secret %s/%s: %v", ingressRoute.Namespace, ingressRoute.Spec.TLS.SecretName, err) - } - if !exists { - return fmt.Errorf("secret %s/%s does not exist", ingressRoute.Namespace, ingressRoute.Spec.TLS.SecretName) - } - - cert, key, err := getCertificateBlocks(secret, ingressRoute.Namespace, ingressRoute.Spec.TLS.SecretName) + tlsConf, err := getTLS(k8sClient, ingressRoute.Spec.TLS.SecretName, ingressRoute.Namespace) if err != nil { return err } - tlsConfigs[configKey] = &tls.Configuration{ - Certificate: &tls.Certificate{ - CertFile: tls.FileOrContent(cert), - KeyFile: tls.FileOrContent(key), - }, - } + tlsConfigs[configKey] = tlsConf } return nil } +func getTLSTCP(ctx context.Context, ingressRoute *v1alpha1.IngressRouteTCP, k8sClient Client, tlsConfigs map[string]*tls.Configuration) error { + if ingressRoute.Spec.TLS == nil { + return nil + } + if ingressRoute.Spec.TLS.SecretName == "" { + log.FromContext(ctx).Debugf("Skipping TLS sub-section for TCP: No secret name provided") + return nil + } + + configKey := ingressRoute.Namespace + "/" + ingressRoute.Spec.TLS.SecretName + if _, tlsExists := tlsConfigs[configKey]; !tlsExists { + tlsConf, err := getTLS(k8sClient, ingressRoute.Spec.TLS.SecretName, ingressRoute.Namespace) + if err != nil { + return err + } + + tlsConfigs[configKey] = tlsConf + } + + return nil +} + +func getTLS(k8sClient Client, secretName, namespace string) (*tls.Configuration, error) { + secret, exists, err := k8sClient.GetSecret(namespace, secretName) + if err != nil { + return nil, fmt.Errorf("failed to fetch secret %s/%s: %v", namespace, secretName, err) + } + if !exists { + return nil, fmt.Errorf("secret %s/%s does not exist", namespace, secretName) + } + + cert, key, err := getCertificateBlocks(secret, namespace, secretName) + if err != nil { + return nil, err + } + + return &tls.Configuration{ + Certificate: &tls.Certificate{ + CertFile: tls.FileOrContent(cert), + KeyFile: tls.FileOrContent(key), + }, + }, nil +} + func getTLSConfig(tlsConfigs map[string]*tls.Configuration) []*tls.Configuration { var secretNames []string for secretName := range tlsConfigs { diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 175b88e59..159b65f2b 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -12,6 +12,346 @@ import ( var _ provider.Provider = (*Provider)(nil) +func TestLoadIngressRouteTCPs(t *testing.T) { + testCases := []struct { + desc string + ingressClass string + paths []string + expected *config.Configuration + }{ + { + desc: "Empty", + expected: &config.Configuration{ + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + { + desc: "Simple Ingress Route, with foo entrypoint", + paths: []string{"tcp/services.yml", "tcp/simple.yml"}, + expected: &config.Configuration{ + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "default/test-crd-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default/test-crd-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + }, + }, + Services: map[string]*config.TCPService{ + "default/test-crd-fdd3e9338e47a45efefc": { + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + { + Address: "10.10.0.2:8000", + Port: "", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "One ingress Route with two different rules", + paths: []string{"tcp/services.yml", "tcp/with_two_rules.yml"}, + expected: &config.Configuration{ + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "default/test-crd-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default/test-crd-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + }, + "default/test-crd-f44ce589164e656d231c": { + EntryPoints: []string{"foo"}, + Service: "default/test-crd-f44ce589164e656d231c", + Rule: "HostSNI(`bar.com`)", + }, + }, + Services: map[string]*config.TCPService{ + "default/test-crd-fdd3e9338e47a45efefc": { + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + { + Address: "10.10.0.2:8000", + Port: "", + }, + }, + }, + }, + "default/test-crd-f44ce589164e656d231c": { + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + { + Address: "10.10.0.2:8000", + Port: "", + }, + }, + }, + }, + }, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + { + desc: "One ingress Route with two different services, their servers will merge", + paths: []string{"tcp/services.yml", "tcp/with_two_services.yml"}, + expected: &config.Configuration{ + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "default/test-crd-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default/test-crd-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + }, + }, + Services: map[string]*config.TCPService{ + "default/test-crd-fdd3e9338e47a45efefc": { + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + { + Address: "10.10.0.2:8000", + Port: "", + }, + { + Address: "10.10.0.3:8080", + Port: "", + }, + { + Address: "10.10.0.4:8080", + Port: "", + }, + }, + }, + }}, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + { + desc: "Ingress class does not match", + paths: []string{"tcp/services.yml", "tcp/simple.yml"}, + ingressClass: "tchouk", + expected: &config.Configuration{ + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + { + desc: "Route with empty rule value is ignored", + paths: []string{"tcp/services.yml", "tcp/with_no_rule_value.yml"}, + expected: &config.Configuration{ + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + { + desc: "check rule quoting validity", + paths: []string{"tcp/services.yml", "tcp/with_bad_host_rule.yml"}, + expected: &config.Configuration{ + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + { + desc: "TLS", + paths: []string{"tcp/services.yml", "tcp/with_tls.yml"}, + expected: &config.Configuration{ + TLS: []*tls.Configuration{ + { + Certificate: &tls.Certificate{ + CertFile: tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), + KeyFile: tls.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"), + }, + }, + }, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "default/test-crd-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default/test-crd-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + TLS: &config.RouterTCPTLSConfig{}, + }, + }, + Services: map[string]*config.TCPService{ + "default/test-crd-fdd3e9338e47a45efefc": { + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + { + Address: "10.10.0.2:8000", + Port: "", + }, + }, + }, + }, + }, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + { + desc: "TLS with passthrough", + paths: []string{"tcp/services.yml", "tcp/with_tls_passthrough.yml"}, + expected: &config.Configuration{ + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "default/test-crd-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default/test-crd-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + TLS: &config.RouterTCPTLSConfig{ + Passthrough: true, + }, + }, + }, + Services: map[string]*config.TCPService{ + "default/test-crd-fdd3e9338e47a45efefc": { + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + { + Address: "10.10.0.2:8000", + Port: "", + }, + }, + }, + }, + }, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + { + desc: "TLS with ACME", + paths: []string{"tcp/services.yml", "tcp/with_tls_acme.yml"}, + expected: &config.Configuration{ + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{ + "default/test-crd-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default/test-crd-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + TLS: &config.RouterTCPTLSConfig{}, + }, + }, + Services: map[string]*config.TCPService{ + "default/test-crd-fdd3e9338e47a45efefc": { + LoadBalancer: &config.TCPLoadBalancerService{ + Servers: []config.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + { + Address: "10.10.0.2:8000", + Port: "", + }, + }, + }, + }, + }, + }, + HTTP: &config.HTTPConfiguration{ + Routers: map[string]*config.Router{}, + Middlewares: map[string]*config.Middleware{}, + Services: map[string]*config.Service{}, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + if test.expected == nil { + return + } + + p := Provider{IngressClass: test.ingressClass} + conf := p.loadConfigurationFromIngresses(context.Background(), newClientMock(test.paths...)) + assert.Equal(t, test.expected, conf) + }) + } +} + func TestLoadIngressRoutes(t *testing.T) { testCases := []struct { desc string @@ -22,7 +362,10 @@ func TestLoadIngressRoutes(t *testing.T) { { desc: "Empty", expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{}, Middlewares: map[string]*config.Middleware{}, @@ -34,19 +377,22 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "Simple Ingress Route, with foo entrypoint", paths: []string{"services.yml", "simple.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { EntryPoints: []string{"foo"}, - Service: "default/test.crd-6b204d94623b3df4370c", + Service: "default/test-crd-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, }, }, Middlewares: map[string]*config.Middleware{}, Services: map[string]*config.Service{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { LoadBalancer: &config.LoadBalancerService{ Servers: []config.Server{ { @@ -67,12 +413,15 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "Simple Ingress Route with middleware", paths: []string{"services.yml", "with_middleware.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{ - "default/test2.crd-23c7f4c450289ee29016": { + "default/test2-crd-23c7f4c450289ee29016": { EntryPoints: []string{"web"}, - Service: "default/test2.crd-23c7f4c450289ee29016", + Service: "default/test2-crd-23c7f4c450289ee29016", Rule: "Host(`foo.com`) && PathPrefix(`/tobestripped`)", Priority: 12, Middlewares: []string{"default/stripprefix", "foo/addprefix"}, @@ -91,7 +440,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, Services: map[string]*config.Service{ - "default/test2.crd-23c7f4c450289ee29016": { + "default/test2-crd-23c7f4c450289ee29016": { LoadBalancer: &config.LoadBalancerService{ Servers: []config.Server{ { @@ -112,25 +461,28 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "One ingress Route with two different rules", paths: []string{"services.yml", "with_two_rules.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", - Service: "default/test.crd-6b204d94623b3df4370c", + Service: "default/test-crd-6b204d94623b3df4370c", Priority: 14, }, - "default/test.crd-77c62dfe9517144aeeaa": { + "default/test-crd-77c62dfe9517144aeeaa": { EntryPoints: []string{"web"}, - Service: "default/test.crd-77c62dfe9517144aeeaa", + Service: "default/test-crd-77c62dfe9517144aeeaa", Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", Priority: 12, }, }, Middlewares: map[string]*config.Middleware{}, Services: map[string]*config.Service{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { LoadBalancer: &config.LoadBalancerService{ Servers: []config.Server{ { @@ -143,7 +495,7 @@ func TestLoadIngressRoutes(t *testing.T) { PassHostHeader: true, }, }, - "default/test.crd-77c62dfe9517144aeeaa": { + "default/test-crd-77c62dfe9517144aeeaa": { LoadBalancer: &config.LoadBalancerService{ Servers: []config.Server{ { @@ -164,19 +516,22 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "One ingress Route with two different services, their servers will merge", paths: []string{"services.yml", "with_two_services.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{ - "default/test.crd-77c62dfe9517144aeeaa": { + "default/test-crd-77c62dfe9517144aeeaa": { EntryPoints: []string{"web"}, - Service: "default/test.crd-77c62dfe9517144aeeaa", + Service: "default/test-crd-77c62dfe9517144aeeaa", Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", Priority: 12, }, }, Middlewares: map[string]*config.Middleware{}, Services: map[string]*config.Service{ - "default/test.crd-77c62dfe9517144aeeaa": { + "default/test-crd-77c62dfe9517144aeeaa": { LoadBalancer: &config.LoadBalancerService{ Servers: []config.Server{ { @@ -204,7 +559,10 @@ func TestLoadIngressRoutes(t *testing.T) { paths: []string{"services.yml", "simple.yml"}, ingressClass: "tchouk", expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{}, Middlewares: map[string]*config.Middleware{}, @@ -216,7 +574,10 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "Route with empty rule value is ignored", paths: []string{"services.yml", "with_no_rule_value.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{}, Middlewares: map[string]*config.Middleware{}, @@ -228,7 +589,10 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "Route with kind not of a rule type (empty kind) is ignored", paths: []string{"services.yml", "with_wrong_rule_kind.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{}, Middlewares: map[string]*config.Middleware{}, @@ -240,7 +604,10 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "check rule quoting validity", paths: []string{"services.yml", "with_bad_host_rule.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{}, Middlewares: map[string]*config.Middleware{}, @@ -260,12 +627,15 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default/test.crd-6b204d94623b3df4370c", + Service: "default/test-crd-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &config.RouterTLSConfig{}, @@ -273,7 +643,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*config.Middleware{}, Services: map[string]*config.Service{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { LoadBalancer: &config.LoadBalancerService{ Servers: []config.Server{ { @@ -294,12 +664,15 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "TLS with ACME", paths: []string{"services.yml", "with_tls_acme.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { EntryPoints: []string{"web"}, - Service: "default/test.crd-6b204d94623b3df4370c", + Service: "default/test-crd-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, TLS: &config.RouterTLSConfig{}, @@ -307,7 +680,7 @@ func TestLoadIngressRoutes(t *testing.T) { }, Middlewares: map[string]*config.Middleware{}, Services: map[string]*config.Service{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { LoadBalancer: &config.LoadBalancerService{ Servers: []config.Server{ { @@ -328,19 +701,22 @@ func TestLoadIngressRoutes(t *testing.T) { desc: "Simple Ingress Route, defaulting to https for servers", paths: []string{"services.yml", "with_https_default.yml"}, expected: &config.Configuration{ - TCP: &config.TCPConfiguration{}, + TCP: &config.TCPConfiguration{ + Routers: map[string]*config.TCPRouter{}, + Services: map[string]*config.TCPService{}, + }, HTTP: &config.HTTPConfiguration{ Routers: map[string]*config.Router{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { EntryPoints: []string{"foo"}, - Service: "default/test.crd-6b204d94623b3df4370c", + Service: "default/test-crd-6b204d94623b3df4370c", Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", Priority: 12, }, }, Middlewares: map[string]*config.Middleware{}, Services: map[string]*config.Service{ - "default/test.crd-6b204d94623b3df4370c": { + "default/test-crd-6b204d94623b3df4370c": { LoadBalancer: &config.LoadBalancerService{ Servers: []config.Server{ { diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go index a7b22f53f..9d37e876c 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go @@ -21,7 +21,12 @@ type Route struct { } // TLS contains the TLS certificates configuration of the routes. To enable -// Let's Encrypt, set a SecretName with an empty value. +// Let's Encrypt, use an empty TLS struct, e.g. in YAML: +// +// tls: {} # inline format +// +// tls: +// secretName: # block format type TLS struct { // SecretName is the name of the referenced Kubernetes Secret to specify the // certificate details. diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go new file mode 100644 index 000000000..0a3ec20c4 --- /dev/null +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go @@ -0,0 +1,58 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// IngressRouteTCPSpec is a specification for a IngressRouteTCPSpec resource. +type IngressRouteTCPSpec struct { + Routes []RouteTCP `json:"routes"` + EntryPoints []string `json:"entryPoints"` + TLS *TLSTCP `json:"tls,omitempty"` +} + +// RouteTCP contains the set of routes. +type RouteTCP struct { + Match string `json:"match"` + 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: +// +// tls: {} # inline format +// +// tls: +// secretName: # block format +type TLSTCP struct { + // SecretName is the name of the referenced Kubernetes Secret to specify the + // certificate details. + SecretName string `json:"secretName"` + Passthrough bool `json:"passthrough"` +} + +// ServiceTCP defines an upstream to proxy traffic. +type ServiceTCP struct { + Name string `json:"name"` + Port int32 `json:"port"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// IngressRouteTCP is an Ingress CRD specification. +type IngressRouteTCP struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + Spec IngressRouteTCPSpec `json:"spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// IngressRouteTCPList is a list of IngressRoutes. +type IngressRouteTCPList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []IngressRouteTCP `json:"items"` +} diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/register.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/register.go index 9fb517b54..b64fdb3ce 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/register.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/register.go @@ -35,6 +35,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &IngressRoute{}, &IngressRouteList{}, + &IngressRouteTCP{}, + &IngressRouteTCPList{}, &Middleware{}, &MiddlewareList{}, ) 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 b07ecee22..afed89fc6 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -148,6 +148,99 @@ func (in *IngressRouteSpec) DeepCopy() *IngressRouteSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRouteTCP) DeepCopyInto(out *IngressRouteTCP) { + *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 IngressRouteTCP. +func (in *IngressRouteTCP) DeepCopy() *IngressRouteTCP { + if in == nil { + return nil + } + out := new(IngressRouteTCP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IngressRouteTCP) 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 *IngressRouteTCPList) DeepCopyInto(out *IngressRouteTCPList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]IngressRouteTCP, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteTCPList. +func (in *IngressRouteTCPList) DeepCopy() *IngressRouteTCPList { + if in == nil { + return nil + } + out := new(IngressRouteTCPList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *IngressRouteTCPList) 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 *IngressRouteTCPSpec) DeepCopyInto(out *IngressRouteTCPSpec) { + *out = *in + if in.Routes != nil { + in, out := &in.Routes, &out.Routes + *out = make([]RouteTCP, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.EntryPoints != nil { + in, out := &in.EntryPoints, &out.EntryPoints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSTCP) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRouteTCPSpec. +func (in *IngressRouteTCPSpec) DeepCopy() *IngressRouteTCPSpec { + if in == nil { + return nil + } + out := new(IngressRouteTCPSpec) + 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 @@ -252,6 +345,27 @@ func (in *Route) DeepCopy() *Route { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RouteTCP) DeepCopyInto(out *RouteTCP) { + *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]ServiceTCP, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteTCP. +func (in *RouteTCP) DeepCopy() *RouteTCP { + if in == nil { + return nil + } + out := new(RouteTCP) + in.DeepCopyInto(out) + return out +} + // 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 @@ -273,6 +387,22 @@ 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 *ServiceTCP) DeepCopyInto(out *ServiceTCP) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTCP. +func (in *ServiceTCP) DeepCopy() *ServiceTCP { + if in == nil { + return nil + } + out := new(ServiceTCP) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLS) DeepCopyInto(out *TLS) { *out = *in @@ -288,3 +418,19 @@ func (in *TLS) DeepCopy() *TLS { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSTCP) DeepCopyInto(out *TLSTCP) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSTCP. +func (in *TLSTCP) DeepCopy() *TLSTCP { + if in == nil { + return nil + } + out := new(TLSTCP) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index b8a43f60a..10b818ea8 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -318,7 +318,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl } serviceName := ingress.Namespace + "/" + p.Backend.ServiceName + "/" + p.Backend.ServicePort.String() - + serviceName = strings.ReplaceAll(serviceName, ".", "-") var rules []string if len(rule.Host) > 0 { rules = []string{"Host(`" + rule.Host + "`)"} diff --git a/pkg/server/service/tcp/service.go b/pkg/server/service/tcp/service.go index 39880c718..9116f4648 100644 --- a/pkg/server/service/tcp/service.go +++ b/pkg/server/service/tcp/service.go @@ -43,7 +43,7 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han loadBalancer := tcp.NewRRLoadBalancer() - for _, server := range conf.LoadBalancer.Servers { + for name, server := range conf.LoadBalancer.Servers { if _, _, err := net.SplitHostPort(server.Address); err != nil { logger.Errorf("In service %q: %v", serviceQualifiedName, err) continue @@ -56,6 +56,7 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han } loadBalancer.AddServer(handler) + logger.WithField(log.ServerName, name).Debugf("Creating TCP server %d at %s", name, server.Address) } return loadBalancer, nil }