Merge branch v2.3 into master

This commit is contained in:
kevinpollet 2020-12-11 10:58:00 +01:00
commit eebbe64b36
No known key found for this signature in database
GPG key ID: 0C9A5DDD1B292453
46 changed files with 1949 additions and 148 deletions

View file

@ -1,3 +1,30 @@
## [v2.3.5](https://github.com/traefik/traefik/tree/v2.3.5) (2020-12-10)
[All Commits](https://github.com/traefik/traefik/compare/v2.3.4...v2.3.5)
**Bug fixes:**
- **[acme]** Update go-acme/lego to v4.1.3 ([#7625](https://github.com/traefik/traefik/pull/7625) by [ldez](https://github.com/ldez))
- **[k8s,k8s/crd]** IngressRoute: add an option to disable cross-namespace routing ([#7595](https://github.com/traefik/traefik/pull/7595) by [rtribotte](https://github.com/rtribotte))
- **[k8s/crd,k8s/ingress]** Fix concatenation of IPv6 addresses and ports ([#7620](https://github.com/traefik/traefik/pull/7620) by [jspdown](https://github.com/jspdown))
- **[tcp,tls]** Fix TLS options fallback when domain and options are the same ([#7609](https://github.com/traefik/traefik/pull/7609) by [jspdown](https://github.com/jspdown))
- **[webui]** Fix UI bug on long service name ([#7535](https://github.com/traefik/traefik/pull/7535) by [ipinak](https://github.com/ipinak))
**Documentation:**
- **[docker]** Add example for multiple service per container ([#7610](https://github.com/traefik/traefik/pull/7610) by [notsureifkevin](https://github.com/notsureifkevin))
- Documentation: Add spacing to sidebars so the last item is always visible ([#7616](https://github.com/traefik/traefik/pull/7616) by [paulocfjunior](https://github.com/paulocfjunior))
- Fix typos in migration guide ([#7596](https://github.com/traefik/traefik/pull/7596) by [marsavela](https://github.com/marsavela))
## [v2.3.4](https://github.com/traefik/traefik/tree/v2.3.4) (2020-11-24)
[All Commits](https://github.com/traefik/traefik/compare/v2.3.3...v2.3.4)
**Bug fixes:**
- **[acme]** Update go-acme/lego to v4.1.2 ([#7577](https://github.com/traefik/traefik/pull/7577) by [ldez](https://github.com/ldez))
- **[k8s,k8s/crd,k8s/ingress]** Apply labelSelector as a TweakListOptions for Kubernetes informers ([#7521](https://github.com/traefik/traefik/pull/7521) by [rtribotte](https://github.com/rtribotte))
- **[middleware]** Do not evaluate templated URL in redirectRegex middleware ([#7573](https://github.com/traefik/traefik/pull/7573) by [jspdown](https://github.com/jspdown))
- **[provider]** fix: invalid slice parsing. ([#7583](https://github.com/traefik/traefik/pull/7583) by [ldez](https://github.com/ldez))
**Documentation:**
- **[ecs]** Fix clusters option in ECS provider documentation ([#7586](https://github.com/traefik/traefik/pull/7586) by [skapin](https://github.com/skapin))
## [v2.3.3](https://github.com/traefik/traefik/tree/v2.3.3) (2020-11-19)
[All Commits](https://github.com/traefik/traefik/compare/v2.3.2...v2.3.3)

View file

@ -35,6 +35,10 @@
padding: 0;
}
.md-sidebar__scrollwrap {
max-height: calc(100% - 50px);
}
.md-sidebar--secondary .md-sidebar__scrollwrap {
border-radius: 8px;
background-color: var(--light-blue) !important;

View file

@ -385,7 +385,7 @@ To apply a redirection:
entryPoints:
web:
address: 80
address: ":80"
http:
redirections:
entrypoint:
@ -393,7 +393,7 @@ To apply a redirection:
scheme: https
websecure:
address: 443
address: ":443"
```
!!! example "HTTP to HTTPS redirection per domain"

View file

@ -87,7 +87,7 @@ _Optional, Default=["default"]_
```toml tab="File (TOML)"
[providers.ecs]
cluster = ["default"]
clusters = ["default"]
# ...
```

View file

@ -250,6 +250,34 @@ providers:
--providers.kubernetescrd.throttleDuration=10s
```
### `allowCrossNamespace`
_Optional, Default: true_
```toml tab="File (TOML)"
[providers.kubernetesCRD]
allowCrossNamespace = false
# ...
```
```yaml tab="File (YAML)"
providers:
kubernetesCRD:
allowCrossNamespace: false
# ...
```
```bash tab="CLI"
--providers.kubernetescrd.allowCrossNamespace=false
```
If the parameter is set to `false`, an IngressRoute will not be able to reference any resources
in another namespace than the IngressRoute namespace.
!!! warning "Deprecation"
Please notice that the default value for this option will be set to `false` in a future version.
## Further
Also see the [full example](../user-guides/crd-acme/index.md) with Let's Encrypt.

View file

@ -549,6 +549,9 @@ TLS key
`--providers.kubernetescrd`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`--providers.kubernetescrd.allowcrossnamespace`:
Allow cross namespace resource reference. (Default: ```true```)
`--providers.kubernetescrd.certauthfilepath`:
Kubernetes certificate authority file path (not needed for in-cluster client).

View file

@ -549,6 +549,9 @@ TLS key
`TRAEFIK_PROVIDERS_KUBERNETESCRD`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWCROSSNAMESPACE`:
Allow cross namespace resource reference. (Default: ```true```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).

View file

@ -114,6 +114,7 @@
certAuthFilePath = "foobar"
disablePassHostHeaders = true
namespaces = ["foobar", "foobar"]
allowCrossNamespace = true
labelSelector = "foobar"
ingressClass = "foobar"
throttleDuration = 42

View file

@ -124,6 +124,7 @@ providers:
namespaces:
- foobar
- foobar
allowCrossNamespace: true
labelSelector: foobar
ingressClass: foobar
throttleDuration: 42s

View file

@ -58,6 +58,26 @@ Attach labels to your containers and let Traefik do the rest!
Setting the label `traefik.http.services.xxx.loadbalancer.server.port`
overrides that behavior.
??? example "Specifying more than one router and service per container"
Forwarding requests to more than one port on a container requires referencing the service loadbalancer port definition using the service parameter on the router.
In this example, requests are forwarded for `http://example-a.com` to `http://<private IP of container>:8000` in addition to `http://example-b.com` forwarding to `http://<private IP of container>:9000`:
```yaml
version: "3"
services:
my-container:
# ...
labels:
- traefik.http.routers.www-router.rule=Host(`example-a.com`)
- traefik.http.routers.www-router.service=www-service
- traefik.http.services.www-service.loadbalancer.server.port=8000
- traefik.http.routers.admin-router.rule=Host(`example-b.com`)
- traefik.http.routers.admin-router.service=admin-service
- traefik.http.services.admin-service.loadbalancer.server.port=9000
```
??? example "Configuring Docker Swarm & Deploying / Exposing Services"
Enabling the docker provider (Swarm Mode)

4
go.mod
View file

@ -33,7 +33,7 @@ require (
github.com/fatih/structs v1.1.0
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2
github.com/go-acme/lego/v4 v4.1.0
github.com/go-acme/lego/v4 v4.1.3
github.com/go-check/check v0.0.0-00010101000000-000000000000
github.com/go-kit/kit v0.10.1-0.20200915143503-439c4d2ed3ea
github.com/golang/protobuf v1.4.2
@ -71,7 +71,7 @@ require (
github.com/stretchr/testify v1.6.1
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154
github.com/tinylib/msgp v1.0.2 // indirect
github.com/traefik/paerser v0.1.0
github.com/traefik/paerser v0.1.1
github.com/traefik/yaegi v0.9.7
github.com/uber/jaeger-client-go v2.25.0+incompatible
github.com/uber/jaeger-lib v2.2.0+incompatible

8
go.sum
View file

@ -284,8 +284,8 @@ github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2/go.mod h1:GLy
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-acme/lego/v4 v4.1.0 h1:/9pMjaeaLq6m0n+io+kv2ySs2ZfrmH6eazuMoN18GHo=
github.com/go-acme/lego/v4 v4.1.0/go.mod h1:pIFm5tWkXSgiAEfJ/XQCQIvX1cEvHFwbgLZyx8OVSUE=
github.com/go-acme/lego/v4 v4.1.3 h1:D8nnzrijQFUAqdNPwnbvm6tJ3AJAzQAlnROeecUNG/4=
github.com/go-acme/lego/v4 v4.1.3/go.mod h1:pIFm5tWkXSgiAEfJ/XQCQIvX1cEvHFwbgLZyx8OVSUE=
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@ -786,8 +786,8 @@ github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/traefik/paerser v0.1.0 h1:B4v1tbvd8YnHsA7spwHKEWJoGrRP+2jYpIozsCMHhl0=
github.com/traefik/paerser v0.1.0/go.mod h1:yYnAgdEC2wJH5CgG75qGWC8SsFDEapg09o9RrA6FfrE=
github.com/traefik/paerser v0.1.1 h1:Suj0iA4hTAV6E4Dh5/++TXAj5u6iTwydBlFssIUz+9w=
github.com/traefik/paerser v0.1.1/go.mod h1:yYnAgdEC2wJH5CgG75qGWC8SsFDEapg09o9RrA6FfrE=
github.com/traefik/yaegi v0.9.7 h1:CbeKjEhy3DoSC8xC4TQF2Mhmd7u3Cjqluz1//x6Vtcs=
github.com/traefik/yaegi v0.9.7/go.mod h1:FAYnRlZyuVlEkvnkHq3bvJ1lW5be6XuwgLdkYgYG6Lk=
github.com/transip/gotransip/v6 v6.2.0 h1:0Z+qVsyeiQdWfcAUeJyF0IEKAPvhJwwpwPi2WGtBIiE=

View file

@ -0,0 +1,120 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: other-ns
---
apiVersion: v1
kind: Service
metadata:
name: whoami
namespace: other-ns
spec:
ports:
- name: http
port: 80
selector:
app: traefiklabs
task: whoami
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test6.route
namespace: other-ns
spec:
entryPoints:
- web
routes:
- match: Host(`foo.com`) && PathPrefix(`/a`)
kind: Rule
services:
- name: whoami
namespace: default
port: 80
- match: Host(`foo.com`) && PathPrefix(`/b`)
kind: Rule
services:
- name: wrr2
namespace: default
kind: TraefikService
- match: Host(`foo.com`) && PathPrefix(`/c`)
kind: Rule
services:
- name: wrr3
kind: TraefikService
- match: Host(`foo.com`) && PathPrefix(`/d`)
kind: Rule
services:
- name: whoami
namespace: other-ns
port: 80
middlewares:
- name: stripprefix2
namespace: default
- match: Host(`foo.com`) && PathPrefix(`/e`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: test-errorpage
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: wrr2
namespace: default
spec:
weighted:
services:
- name: whoami
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: wrr3
namespace: other-ns
spec:
weighted:
services:
- name: whoami
namespace: default
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: stripprefix2
namespace: default
spec:
stripPrefix:
prefixes:
- /tobestripped
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: test-errorpage
spec:
errors:
status:
- 500-599
query: /{status}.html
service:
name: whoami
namespace: other-ns
port: 80

View file

@ -16,3 +16,4 @@
address = ":8000"
[providers.kubernetesCRD]
allowCrossNamespace = false

View file

@ -53,6 +53,7 @@ func (s *K8sSuite) TearDownSuite(c *check.C) {
"./fixtures/k8s/coredns.yaml",
"./fixtures/k8s/rolebindings.yaml",
"./fixtures/k8s/traefik.yaml",
"./fixtures/k8s/ccm.yaml",
}
for _, filename := range generatedFiles {

View file

@ -61,6 +61,20 @@
"using": [
"web"
]
},
"other-ns-test6-route-482e4988e134701d8cc8@kubernetescrd": {
"entryPoints": [
"web"
],
"service": "other-ns-wrr3",
"rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/c`)",
"error": [
"the service \"other-ns-wrr3@kubernetescrd\" does not exist"
],
"status": "disabled",
"using": [
"web"
]
}
},
"middlewares": {
@ -75,6 +89,14 @@
"default-test2-route-23c7f4c450289ee29016@kubernetescrd"
]
},
"default-stripprefix2@kubernetescrd": {
"stripPrefix": {
"prefixes": [
"/tobestripped"
]
},
"status": "enabled"
},
"default-stripprefix@kubernetescrd": {
"stripPrefix": {
"prefixes": [
@ -205,6 +227,17 @@
"default-test3-route-7d0ac22d3d8db4b82618@kubernetescrd"
]
},
"default-wrr2@kubernetescrd": {
"weighted": {
"services": [
{
"name": "default-whoami-80",
"weight": 1
}
]
},
"status": "enabled"
},
"noop@internal": {
"status": "enabled"
}

View file

@ -1,9 +1,6 @@
package redirect
import (
"bytes"
"html/template"
"io"
"net/http"
"net/url"
"regexp"
@ -47,24 +44,17 @@ func (r *redirect) GetTracingInformation() (string, ext.SpanKindEnum) {
func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
oldURL := rawURL(req)
// If the Regexp doesn't match, skip to the next handler
// If the Regexp doesn't match, skip to the next handler.
if !r.regex.MatchString(oldURL) {
r.next.ServeHTTP(rw, req)
return
}
// apply a rewrite regexp to the URL
// Apply a rewrite regexp to the URL.
newURL := r.regex.ReplaceAllString(oldURL, r.replacement)
// replace any variables that may be in there
rewrittenURL := &bytes.Buffer{}
if err := applyString(newURL, rewrittenURL, req); err != nil {
r.errHandler.ServeHTTP(rw, req, err)
return
}
// parse the rewritten URL and replace request URL with it
parsedURL, err := url.Parse(rewrittenURL.String())
// Parse the rewritten URL and replace request URL with it.
parsedURL, err := url.Parse(newURL)
if err != nil {
r.errHandler.ServeHTTP(rw, req, err)
return
@ -78,7 +68,7 @@ func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
req.URL = parsedURL
// make sure the request URI corresponds the rewritten URL
// Make sure the request URI corresponds the rewritten URL.
req.RequestURI = req.URL.RequestURI()
r.next.ServeHTTP(rw, req)
}
@ -138,14 +128,3 @@ func rawURL(req *http.Request) string {
return strings.Join([]string{scheme, "://", host, port, uri}, "")
}
func applyString(in string, out io.Writer, req *http.Request) error {
t, err := template.New("t").Parse(in)
if err != nil {
return err
}
data := struct{ Request *http.Request }{Request: req}
return t.Execute(out, data)
}

View file

@ -10,7 +10,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/testhelpers"
)
func TestRedirectRegexHandler(t *testing.T) {
@ -35,16 +34,6 @@ func TestRedirectRegexHandler(t *testing.T) {
expectedURL: "https://foobar.com:443",
expectedStatus: http.StatusFound,
},
{
desc: "use request header",
config: dynamic.RedirectRegex{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: `https://${1}{{ .Request.Header.Get "X-Foo" }}$2:443$4`,
},
url: "http://foo.com:80",
expectedURL: "https://foobar.com:443",
expectedStatus: http.StatusFound,
},
{
desc: "URL doesn't match regex",
config: dynamic.RedirectRegex{
@ -186,7 +175,7 @@ func TestRedirectRegexHandler(t *testing.T) {
method = test.method
}
req := testhelpers.MustNewRequest(method, test.url, nil)
req := httptest.NewRequest(method, test.url, nil)
if test.secured {
req.TLS = &tls.ConnectionState{}
}

View file

@ -119,6 +119,35 @@ subsets:
- name: websecure2
port: 8443
---
apiVersion: v1
kind: Service
metadata:
name: whoami-ipv6
namespace: default
spec:
ports:
- name: web
port: 8080
selector:
app: traefiklabs
task: whoami-ipv6
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoami-ipv6
namespace: default
subsets:
- addresses:
- ip: "2001:db8:85a3:8d3:1319:8a2e:370:7348"
ports:
- name: web
port: 8080
---
apiVersion: v1
kind: Service
@ -157,5 +186,46 @@ spec:
protocol: TCP
port: 443
---
apiVersion: v1
kind: Service
metadata:
name: external-svc-with-ipv6
namespace: default
spec:
externalName: "2001:db8:85a3:8d3:1319:8a2e:370:7347"
type: ExternalName
ports:
- name: http
protocol: TCP
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: whoami-svc
namespace: cross-ns
spec:
ports:
- name: web
port: 80
selector:
app: traefiklabs
task: whoami
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoami-svc
namespace: cross-ns
subsets:
- addresses:
- ip: 10.10.0.1
- ip: 10.10.0.2
ports:
- name: web
port: 80

View file

@ -132,6 +132,36 @@ subsets:
- name: myapp4
port: 8084
---
apiVersion: v1
kind: Service
metadata:
name: whoamitcp-ipv6
namespace: default
spec:
ports:
- name: myapp-ipv6
port: 8080
selector:
app: traefiklabs
task: whoamitcp-ipv6
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoamitcp-ipv6
namespace: default
subsets:
- addresses:
- ip: "fd00:10:244:0:1::3"
- ip: "2001:db8:85a3:8d3:1319:8a2e:370:7348"
ports:
- name: myapp-ipv6
port: 8080
---
apiVersion: v1
kind: Service
@ -167,4 +197,44 @@ spec:
type: ExternalName
ports:
- name: http
protocol: TCP
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: external.service.with.ipv6
namespace: default
spec:
externalName: "fe80::200:5aee:feaa:20a2"
type: ExternalName
---
apiVersion: v1
kind: Service
metadata:
name: whoamitcp-cross-ns
namespace: cross-ns
spec:
ports:
- name: myapp
port: 8000
selector:
app: traefiklabs
task: whoamitcp
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoamitcp-cross-ns
namespace: cross-ns
subsets:
- addresses:
- ip: 10.10.0.1
- ip: 10.10.0.2
ports:
- name: myapp
port: 8000

View file

@ -0,0 +1,16 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: HostSNI(`foo.com`)
services:
- name: whoamitcp-cross-ns
namespace: cross-ns
port: 8000

View file

@ -0,0 +1,17 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: HostSNI(`*`)
services:
- name: whoamitcp-ipv6
port: 8080
- name: external.service.with.ipv6
port: 8080

View file

@ -101,3 +101,62 @@ subsets:
ports:
- name: myapp4
port: 8084
---
apiVersion: v1
kind: Service
metadata:
name: whoamiudp-ipv6
namespace: default
spec:
ports:
- name: myapp-ipv6
port: 8080
selector:
app: traefiklabs
task: whoamiudp-ipv6
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoamiudp-ipv6
namespace: default
subsets:
- addresses:
- ip: "fd00:10:244:0:1::3"
ports:
- name: myapp-ipv6
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: whoamiudp-cross-ns
namespace: cross-ns
spec:
ports:
- name: myapp
port: 8000
selector:
app: traefiklabs
task: whoamiudp
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoamiudp-cross-ns
namespace: cross-ns
subsets:
- addresses:
- ip: 10.10.0.1
- ip: 10.10.0.2
ports:
- name: myapp
port: 8000

View file

@ -0,0 +1,15 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteUDP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- services:
- name: whoamiudp-cross-ns
namespace: cross-ns
port: 8000

View file

@ -0,0 +1,14 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteUDP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- services:
- name: whoamiudp-ipv6
port: 8080

View file

@ -0,0 +1,91 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: cross-ns-route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
services:
- name: whoami-svc
namespace: cross-ns
port: 80
- name: tr-svc-wrr1
kind: TraefikService
- name: tr-svc-wrr2
namespace: cross-ns
kind: TraefikService
- name: tr-svc-mirror1
kind: TraefikService
- name: tr-svc-mirror2
namespace: cross-ns
kind: TraefikService
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: tr-svc-wrr1
namespace: default
spec:
weighted:
services:
- name: whoami-svc
namespace: cross-ns
weight: 1
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: tr-svc-wrr2
namespace: cross-ns
spec:
weighted:
services:
- name: whoami-svc
weight: 1
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: tr-svc-mirror1
namespace: default
spec:
mirroring:
name: whoami
port: 80
mirrors:
- name: whoami-svc
namespace: cross-ns
percent: 20
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: tr-svc-mirror2
namespace: cross-ns
spec:
mirroring:
name: whoami-svc
port: 80
mirrors:
- name: whoami-svc
namespace: cross-ns
percent: 20
port: 80

View file

@ -0,0 +1,18 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
services:
- name: whoami-ipv6
port: 8080
- name: external-svc-with-ipv6
port: 8080

View file

@ -0,0 +1,58 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test-crossnamespace.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
services:
- name: whoami
namespace: default
port: 80
middlewares:
- name: stripprefix
namespace: cross-ns
- match: Host(`foo.com`) && PathPrefix(`/bir`)
kind: Rule
priority: 12
services:
- name: whoami
namespace: default
port: 80
middlewares:
- name: test-errorpage
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: stripprefix
namespace: cross-ns
spec:
stripPrefix:
prefixes:
- /stripit
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: test-errorpage
namespace: default
spec:
errors:
status:
- 500-599
query: /{status}.html
service:
name: whoami-svc
namespace: cross-ns
port: 80

View file

@ -43,12 +43,18 @@ type Provider struct {
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers." json:"disablePassHostHeaders,omitempty" toml:"disablePassHostHeaders,omitempty" yaml:"disablePassHostHeaders,omitempty" export:"true"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
AllowCrossNamespace *bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
lastConfiguration safe.Safe
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
p.AllowCrossNamespace = func(b bool) *bool { return &b }(true)
}
func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
_, err := labels.Parse(p.LabelSelector)
if err != nil {
@ -98,6 +104,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
return err
}
if p.AllowCrossNamespace == nil || *p.AllowCrossNamespace {
logger.Warn("Cross-namespace reference between IngressRoutes and resources is enabled, please ensure that this is expected (see AllowCrossNamespace option)")
}
pool.GoCtx(func(ctxPool context.Context) {
operation := func() error {
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
@ -197,7 +207,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
continue
}
errorPage, errorPageService, err := createErrorPageMiddleware(client, middleware.Namespace, middleware.Spec.Errors)
errorPage, errorPageService, err := p.createErrorPageMiddleware(client, middleware.Namespace, middleware.Spec.Errors)
if err != nil {
log.FromContext(ctxMid).Errorf("Error while reading error page middleware: %v", err)
continue
@ -236,7 +246,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
}
}
cb := configBuilder{client}
cb := configBuilder{client, p.AllowCrossNamespace}
for _, service := range client.GetTraefikServices() {
err := cb.buildTraefikService(ctx, service, conf.HTTP.Services)
@ -346,7 +356,7 @@ func getServicePort(svc *corev1.Service, port int32) (*corev1.ServicePort, error
return &corev1.ServicePort{Port: port}, nil
}
func createErrorPageMiddleware(client Client, namespace string, errorPage *v1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) {
func (p *Provider) createErrorPageMiddleware(client Client, namespace string, errorPage *v1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) {
if errorPage == nil {
return nil, nil, nil
}
@ -356,7 +366,7 @@ func createErrorPageMiddleware(client Client, namespace string, errorPage *v1alp
Query: errorPage.Query,
}
balancerServerHTTP, err := configBuilder{client}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec)
balancerServerHTTP, err := configBuilder{client, p.AllowCrossNamespace}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec)
if err != nil {
return nil, nil, err
}
@ -816,3 +826,8 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *s
return eventsChanBuffered
}
func isNamespaceAllowed(allowCrossNamespace *bool, parentNamespace, namespace string) bool {
// If allowCrossNamespace option is not defined the default behavior is to allow cross namespace references.
return allowCrossNamespace == nil || *allowCrossNamespace || parentNamespace == namespace
}

View file

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
@ -47,7 +49,8 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
ingressName = ingressRoute.GenerateName
}
cb := configBuilder{client}
cb := configBuilder{client, p.AllowCrossNamespace}
for _, route := range ingressRoute.Spec.Routes {
if route.Kind != "Rule" {
logger.Errorf("Unsupported match kind: %s. Only \"Rule\" is supported for now.", route.Kind)
@ -65,23 +68,10 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
continue
}
var mds []string
for _, mi := range route.Middlewares {
if strings.Contains(mi.Name, providerNamespaceSeparator) {
if len(mi.Namespace) > 0 {
logger.
WithField(log.MiddlewareName, mi.Name).
Warnf("namespace %q is ignored in cross-provider context", mi.Namespace)
}
mds = append(mds, mi.Name)
continue
}
ns := mi.Namespace
if len(ns) == 0 {
ns = ingressRoute.Namespace
}
mds = append(mds, makeID(ns, mi.Name))
mds, err := p.makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares)
if err != nil {
logger.Errorf("Failed to create middleware keys: %v", err)
continue
}
normalized := provider.Normalize(makeID(ingressRoute.Namespace, serviceKey))
@ -152,8 +142,38 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
return conf
}
func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace string, middlewares []v1alpha1.MiddlewareRef) ([]string, error) {
var mds []string
for _, mi := range middlewares {
if strings.Contains(mi.Name, providerNamespaceSeparator) {
if len(mi.Namespace) > 0 {
log.FromContext(ctx).
WithField(log.MiddlewareName, mi.Name).
Warnf("namespace %q is ignored in cross-provider context", mi.Namespace)
}
mds = append(mds, mi.Name)
continue
}
ns := ingRouteNamespace
if len(mi.Namespace) > 0 {
if !isNamespaceAllowed(p.AllowCrossNamespace, ingRouteNamespace, mi.Namespace) {
return nil, fmt.Errorf("middleware %s/%s is not in the IngressRoute namespace %s", mi.Namespace, mi.Name, ingRouteNamespace)
}
ns = mi.Namespace
}
mds = append(mds, makeID(ns, mi.Name))
}
return mds, nil
}
type configBuilder struct {
client Client
client Client
allowCrossNamespace *bool
}
// buildTraefikService creates the configuration for the traefik service defined in tService,
@ -270,7 +290,7 @@ func (c configBuilder) buildServersLB(namespace string, svc v1alpha1.LoadBalance
return &dynamic.Service{LoadBalancer: lb}, nil
}
func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBalancerSpec) ([]dynamic.Server, error) {
func (c configBuilder) loadServers(parentNamespace string, svc v1alpha1.LoadBalancerSpec) ([]dynamic.Server, error) {
strategy := svc.Strategy
if strategy == "" {
strategy = roundRobinStrategy
@ -279,7 +299,11 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa
return nil, fmt.Errorf("load balancing strategy %s is not supported", strategy)
}
namespace := namespaceOrFallback(svc, fallbackNamespace)
namespace := namespaceOrFallback(svc, parentNamespace)
if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, namespace) {
return nil, fmt.Errorf("load balancer service %s/%s is not in the parent resource namespace %s", svc.Namespace, svc.Name, parentNamespace)
}
// If the service uses explicitly the provider suffix
sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+providerName)
@ -303,8 +327,10 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa
return nil, err
}
hostPort := net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port)))
return append(servers, dynamic.Server{
URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, svcPort.Port),
URL: fmt.Sprintf("%s://%s", protocol, hostPort),
}), nil
}
@ -338,8 +364,10 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa
}
for _, addr := range subset.Addresses {
hostPort := net.JoinHostPort(addr.IP, strconv.Itoa(int(port)))
servers = append(servers, dynamic.Server{
URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port),
URL: fmt.Sprintf("%s://%s", protocol, hostPort),
})
}
}
@ -351,10 +379,14 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa
// In addition, if the service is a Kubernetes one,
// it generates and returns the configuration part for such a service,
// so that the caller can add it to the global config map.
func (c configBuilder) nameAndService(ctx context.Context, namespaceService string, service v1alpha1.LoadBalancerSpec) (string, *dynamic.Service, error) {
func (c configBuilder) nameAndService(ctx context.Context, parentNamespace string, service v1alpha1.LoadBalancerSpec) (string, *dynamic.Service, error) {
svcCtx := log.With(ctx, log.Str(log.ServiceName, service.Name))
namespace := namespaceOrFallback(service, namespaceService)
namespace := namespaceOrFallback(service, parentNamespace)
if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, namespace) {
return "", nil, fmt.Errorf("service %s/%s not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
}
switch {
case service.Kind == "" || service.Kind == "Service":

View file

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
@ -53,7 +55,7 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
serviceName := makeID(ingressRouteTCP.Namespace, key)
for _, service := range route.Services {
balancerServerTCP, err := createLoadBalancerServerTCP(client, ingressRouteTCP.Namespace, service)
balancerServerTCP, err := p.createLoadBalancerServerTCP(client, ingressRouteTCP.Namespace, service)
if err != nil {
logger.
WithField("serviceName", service.Name).
@ -123,9 +125,13 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
return conf
}
func createLoadBalancerServerTCP(client Client, namespace string, service v1alpha1.ServiceTCP) (*dynamic.TCPService, error) {
ns := namespace
func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace string, service v1alpha1.ServiceTCP) (*dynamic.TCPService, error) {
ns := parentNamespace
if len(service.Namespace) > 0 {
if !isNamespaceAllowed(p.AllowCrossNamespace, parentNamespace, service.Namespace) {
return nil, fmt.Errorf("tcp service %s/%s is not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
}
ns = service.Namespace
}
@ -174,7 +180,7 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([
var servers []dynamic.TCPServer
if service.Spec.Type == corev1.ServiceTypeExternalName {
servers = append(servers, dynamic.TCPServer{
Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, svcPort.Port),
Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))),
})
} else {
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name)
@ -205,7 +211,7 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([
for _, addr := range subset.Addresses {
servers = append(servers, dynamic.TCPServer{
Address: fmt.Sprintf("%s:%d", addr.IP, port),
Address: net.JoinHostPort(addr.IP, strconv.Itoa(int(port))),
})
}
}

View file

@ -2,15 +2,23 @@ package crd
import (
"context"
"io/ioutil"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/provider"
crdfake "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/generated/clientset/versioned/fake"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/k8s"
"github.com/traefik/traefik/v2/pkg/tls"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
kubefake "k8s.io/client-go/kubernetes/fake"
)
var _ provider.Provider = (*Provider)(nil)
@ -1010,6 +1018,129 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Ingress Route with IPv6 backends",
paths: []string{
"services.yml", "with_ipv6.yml",
"tcp/services.yml", "tcp/with_ipv6.yml",
"udp/services.yml", "udp/with_ipv6.yml",
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"default-test.route-0": {
EntryPoints: []string{"foo"},
Service: "default-test.route-0",
},
},
Services: map[string]*dynamic.UDPService{
"default-test.route-0": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{
Address: "[fd00:10:244:0:1::3]:8080",
},
},
},
},
},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"default-test.route-673acf455cb2dab0b43a": {
EntryPoints: []string{"foo"},
Service: "default-test.route-673acf455cb2dab0b43a",
Rule: "HostSNI(`*`)",
},
},
Services: map[string]*dynamic.TCPService{
"default-test.route-673acf455cb2dab0b43a": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{
{
Name: "default-test.route-673acf455cb2dab0b43a-whoamitcp-ipv6-8080",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "default-test.route-673acf455cb2dab0b43a-external.service.with.ipv6-8080",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"default-test.route-673acf455cb2dab0b43a-whoamitcp-ipv6-8080": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "[fd00:10:244:0:1::3]:8080",
},
{
Address: "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080",
},
},
},
},
"default-test.route-673acf455cb2dab0b43a-external.service.with.ipv6-8080": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "[fe80::200:5aee:feaa:20a2]:8080",
},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{
"default-test-route-6b204d94623b3df4370c": {
EntryPoints: []string{"foo"},
Service: "default-test-route-6b204d94623b3df4370c",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`)",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-whoami-ipv6-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080",
},
},
PassHostHeader: func(i bool) *bool { return &i }(true),
},
},
"default-external-svc-with-ipv6-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://[2001:db8:85a3:8d3:1319:8a2e:370:7347]:8080",
},
},
PassHostHeader: func(i bool) *bool { return &i }(true),
},
},
"default-test-route-6b204d94623b3df4370c": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-ipv6-8080",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "default-external-svc-with-ipv6-8080",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {
@ -1023,7 +1154,10 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
}
p := Provider{IngressClass: test.ingressClass}
conf := p.loadConfigurationFromCRD(context.Background(), newClientMock(test.paths...))
p.SetDefaults()
clientMock := newClientMock(test.paths...)
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
})
}
@ -3205,7 +3339,10 @@ func TestLoadIngressRoutes(t *testing.T) {
}
p := Provider{IngressClass: test.ingressClass}
conf := p.loadConfigurationFromCRD(context.Background(), newClientMock(test.paths...))
p.SetDefaults()
clientMock := newClientMock(test.paths...)
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
})
}
@ -3519,7 +3656,10 @@ func TestLoadIngressRouteUDPs(t *testing.T) {
}
p := Provider{IngressClass: test.ingressClass}
conf := p.loadConfigurationFromCRD(context.Background(), newClientMock(test.paths...))
p.SetDefaults()
clientMock := newClientMock(test.paths...)
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
})
}
@ -3738,3 +3878,572 @@ func TestGetServicePort(t *testing.T) {
})
}
}
func TestCrossNamespace(t *testing.T) {
testCases := []struct {
desc string
allowCrossNamespace bool
ingressClass string
paths []string
expected *dynamic.Configuration
}{
{
desc: "Empty",
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP middleware cross namespace disallowed",
paths: []string{"services.yml", "with_middleware_cross_namespace.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{
"default-test-crossnamespace-route-9313b71dbe6a649d5049": {
EntryPoints: []string{"foo"},
Service: "default-test-crossnamespace-route-9313b71dbe6a649d5049",
Rule: "Host(`foo.com`) && PathPrefix(`/bir`)",
Priority: 12,
Middlewares: []string{"default-test-errorpage"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"cross-ns-stripprefix": {
StripPrefix: &dynamic.StripPrefix{
Prefixes: []string{"/stripit"},
ForceSlash: false,
},
},
},
Services: map[string]*dynamic.Service{
"default-test-crossnamespace-route-9313b71dbe6a649d5049": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP middleware cross namespace allowed",
paths: []string{"services.yml", "with_middleware_cross_namespace.yml"},
allowCrossNamespace: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{
"default-test-crossnamespace-route-6b204d94623b3df4370c": {
EntryPoints: []string{"foo"},
Service: "default-test-crossnamespace-route-6b204d94623b3df4370c",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`)",
Priority: 12,
Middlewares: []string{
"cross-ns-stripprefix",
},
},
"default-test-crossnamespace-route-9313b71dbe6a649d5049": {
EntryPoints: []string{"foo"},
Service: "default-test-crossnamespace-route-9313b71dbe6a649d5049",
Rule: "Host(`foo.com`) && PathPrefix(`/bir`)",
Priority: 12,
Middlewares: []string{"default-test-errorpage"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"cross-ns-stripprefix": {
StripPrefix: &dynamic.StripPrefix{
Prefixes: []string{"/stripit"},
ForceSlash: false,
},
},
"default-test-errorpage": {
Errors: &dynamic.ErrorPage{
Status: []string{"500-599"},
Service: "default-test-errorpage-errorpage-service",
Query: "/{status}.html",
},
},
},
Services: map[string]*dynamic.Service{
"default-test-crossnamespace-route-6b204d94623b3df4370c": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
"default-test-crossnamespace-route-9313b71dbe6a649d5049": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
"default-test-errorpage-errorpage-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP cross namespace allowed",
paths: []string{"services.yml", "with_cross_namespace.yml"},
allowCrossNamespace: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{
"default-cross-ns-route-6b204d94623b3df4370c": {
EntryPoints: []string{"foo"},
Service: "default-cross-ns-route-6b204d94623b3df4370c",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`)",
Priority: 12,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-cross-ns-route-6b204d94623b3df4370c": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "cross-ns-whoami-svc-80",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "default-tr-svc-wrr1",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "cross-ns-tr-svc-wrr2",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "default-tr-svc-mirror1",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "cross-ns-tr-svc-mirror2",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"cross-ns-whoami-svc-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
"default-tr-svc-wrr1": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "cross-ns-whoami-svc-80",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"cross-ns-tr-svc-wrr2": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "cross-ns-whoami-svc-80",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"default-tr-svc-mirror1": {
Mirroring: &dynamic.Mirroring{
Service: "default-whoami-80",
Mirrors: []dynamic.MirrorService{
{
Name: "cross-ns-whoami-svc-80",
Percent: 20,
},
},
},
},
"cross-ns-tr-svc-mirror2": {
Mirroring: &dynamic.Mirroring{
Service: "cross-ns-whoami-svc-80",
Mirrors: []dynamic.MirrorService{
{
Name: "cross-ns-whoami-svc-80",
Percent: 20,
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP cross namespace disallowed",
paths: []string{"services.yml", "with_cross_namespace.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"cross-ns-tr-svc-wrr2": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "cross-ns-whoami-svc-80",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"cross-ns-whoami-svc-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
"cross-ns-tr-svc-mirror2": {
Mirroring: &dynamic.Mirroring{
Service: "cross-ns-whoami-svc-80",
Mirrors: []dynamic.MirrorService{
{
Name: "cross-ns-whoami-svc-80",
Percent: 20,
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP cross namespace allowed",
paths: []string{"tcp/services.yml", "tcp/with_cross_namespace.yml"},
allowCrossNamespace: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"default-test.route-fdd3e9338e47a45efefc": {
EntryPoints: []string{"foo"},
Service: "default-test.route-fdd3e9338e47a45efefc",
Rule: "HostSNI(`foo.com`)",
},
},
Services: map[string]*dynamic.TCPService{
"default-test.route-fdd3e9338e47a45efefc": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:8000",
Port: "",
},
{
Address: "10.10.0.2:8000",
Port: "",
},
},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP cross namespace disallowed",
paths: []string{"tcp/services.yml", "tcp/with_cross_namespace.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
// The router that references the invalid service will be discarded.
Routers: map[string]*dynamic.TCPRouter{
"default-test.route-fdd3e9338e47a45efefc": {
EntryPoints: []string{"foo"},
Service: "default-test.route-fdd3e9338e47a45efefc",
Rule: "HostSNI(`foo.com`)",
},
},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "UDP cross namespace allowed",
paths: []string{"udp/services.yml", "udp/with_cross_namespace.yml"},
allowCrossNamespace: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"default-test.route-0": {
EntryPoints: []string{"foo"},
Service: "default-test.route-0",
},
},
Services: map[string]*dynamic.UDPService{
"default-test.route-0": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{
Address: "10.10.0.1:8000",
Port: "",
},
{
Address: "10.10.0.2:8000",
Port: "",
},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "UDP cross namespace disallowed",
paths: []string{"udp/services.yml", "udp/with_cross_namespace.yml"},
expected: &dynamic.Configuration{
// The router that references the invalid service will be discarded.
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"default-test.route-0": {
EntryPoints: []string{"foo"},
Service: "default-test.route-0",
},
},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var k8sObjects []runtime.Object
var crdObjects []runtime.Object
for _, path := range test.paths {
yamlContent, err := ioutil.ReadFile(filepath.FromSlash("./fixtures/" + path))
if err != nil {
panic(err)
}
objects := k8s.MustParseYaml(yamlContent)
for _, obj := range objects {
switch o := obj.(type) {
case *corev1.Service, *corev1.Endpoints, *corev1.Secret:
k8sObjects = append(k8sObjects, o)
case *v1alpha1.IngressRoute:
crdObjects = append(crdObjects, o)
case *v1alpha1.IngressRouteTCP:
crdObjects = append(crdObjects, o)
case *v1alpha1.IngressRouteUDP:
crdObjects = append(crdObjects, o)
case *v1alpha1.Middleware:
crdObjects = append(crdObjects, o)
case *v1alpha1.TraefikService:
crdObjects = append(crdObjects, o)
case *v1alpha1.TLSOption:
crdObjects = append(crdObjects, o)
case *v1alpha1.TLSStore:
crdObjects = append(crdObjects, o)
default:
}
}
}
kubeClient := kubefake.NewSimpleClientset(k8sObjects...)
crdClient := crdfake.NewSimpleClientset(crdObjects...)
client := newClientImpl(kubeClient, crdClient)
stopCh := make(chan struct{})
eventCh, err := client.WatchAll([]string{"default", "cross-ns"}, stopCh)
require.NoError(t, err)
if k8sObjects != nil || crdObjects != nil {
// just wait for the first event
<-eventCh
}
p := Provider{}
p.SetDefaults()
p.AllowCrossNamespace = func(b bool) *bool { return &b }(test.allowCrossNamespace)
conf := p.loadConfigurationFromCRD(context.Background(), client)
assert.Equal(t, test.expected, conf)
})
}
}

View file

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"net"
"strconv"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
@ -34,7 +36,7 @@ func (p *Provider) loadIngressRouteUDPConfiguration(ctx context.Context, client
serviceName := makeID(ingressRouteUDP.Namespace, key)
for _, service := range route.Services {
balancerServerUDP, err := createLoadBalancerServerUDP(client, ingressRouteUDP.Namespace, service)
balancerServerUDP, err := p.createLoadBalancerServerUDP(client, ingressRouteUDP.Namespace, service)
if err != nil {
logger.
WithField("serviceName", service.Name).
@ -75,9 +77,13 @@ func (p *Provider) loadIngressRouteUDPConfiguration(ctx context.Context, client
return conf
}
func createLoadBalancerServerUDP(client Client, namespace string, service v1alpha1.ServiceUDP) (*dynamic.UDPService, error) {
ns := namespace
func (p *Provider) createLoadBalancerServerUDP(client Client, parentNamespace string, service v1alpha1.ServiceUDP) (*dynamic.UDPService, error) {
ns := parentNamespace
if len(service.Namespace) > 0 {
if !isNamespaceAllowed(p.AllowCrossNamespace, parentNamespace, service.Namespace) {
return nil, fmt.Errorf("udp service %s/%s is not in the parent resource namespace %s", service.Namespace, service.Name, ns)
}
ns = service.Namespace
}
@ -121,7 +127,7 @@ func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([
var servers []dynamic.UDPServer
if service.Spec.Type == corev1.ServiceTypeExternalName {
servers = append(servers, dynamic.UDPServer{
Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, portSpec.Port),
Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(portSpec.Port))),
})
} else {
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name)
@ -152,7 +158,7 @@ func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([
for _, addr := range subset.Addresses {
servers = append(servers, dynamic.UDPServer{
Address: fmt.Sprintf("%s:%d", addr.IP, port),
Address: net.JoinHostPort(addr.IP, strconv.Itoa(int(port))),
})
}
}

View file

@ -0,0 +1,12 @@
kind: Endpoints
apiVersion: v1
metadata:
name: service-bar
namespace: testing
subsets:
- addresses:
- ip: "2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b"
ports:
- name: http
port: 8080

View file

@ -0,0 +1,18 @@
kind: Ingress
apiVersion: networking.k8s.io/v1beta1
metadata:
name: example.com
namespace: testing
spec:
rules:
- http:
paths:
- path: /bar
backend:
serviceName: service-bar
servicePort: 8080
- path: /foo
backend:
serviceName: service-foo
servicePort: 8080

View file

@ -0,0 +1,26 @@
kind: Service
apiVersion: v1
metadata:
name: service-bar
namespace: testing
spec:
ports:
- name: http
port: 8080
clusterIp: "fc00:f853:ccd:e793::1"
type: ClusterIP
---
kind: Service
apiVersion: v1
metadata:
name: service-foo
namespace: testing
spec:
ports:
- name: http
port: 8080
type: ExternalName
externalName: "2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b"

View file

@ -5,8 +5,10 @@ import (
"errors"
"fmt"
"math"
"net"
"os"
"sort"
"strconv"
"strings"
"time"
@ -479,9 +481,10 @@ func loadService(client Client, namespace string, backend networkingv1beta1.Ingr
if service.Spec.Type == corev1.ServiceTypeExternalName {
protocol := getProtocol(portSpec, portSpec.Name, svcConfig)
hostPort := net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(portSpec.Port)))
svc.LoadBalancer.Servers = []dynamic.Server{
{URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, portSpec.Port)},
{URL: fmt.Sprintf("%s://%s", protocol, hostPort)},
}
return svc, nil
@ -516,8 +519,10 @@ func loadService(client Client, namespace string, backend networkingv1beta1.Ingr
protocol := getProtocol(portSpec, portName, svcConfig)
for _, addr := range subset.Addresses {
hostPort := net.JoinHostPort(addr.IP, strconv.Itoa(int(port)))
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port),
URL: fmt.Sprintf("%s://%s", protocol, hostPort),
})
}
}

View file

@ -661,6 +661,47 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
},
},
},
{
desc: "Ingress with IPv6 endpoints",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
"example-com-testing-bar": {
Rule: "PathPrefix(`/bar`)",
Service: "testing-service-bar-8080",
},
"example-com-testing-foo": {
Rule: "PathPrefix(`/foo`)",
Service: "testing-service-foo-8080",
},
},
Services: map[string]*dynamic.Service{
"testing-service-bar-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b]:8080",
},
},
PassHostHeader: Bool(true),
},
},
"testing-service-foo-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b]:8080",
},
},
PassHostHeader: Bool(true),
},
},
},
},
},
},
{
desc: "TLS support",
expected: &dynamic.Configuration{

View file

@ -109,13 +109,18 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
tlsOptionsForHost := map[string]string{}
for routerHTTPName, routerHTTPConfig := range configsHTTP {
if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName {
if routerHTTPConfig.TLS == nil {
continue
}
ctxRouter := log.With(provider.AddInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName))
logger := log.FromContext(ctxRouter)
tlsOptionsName := defaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != defaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
}
domains, err := rules.ParseDomains(routerHTTPConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err)
@ -129,34 +134,27 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
}
for _, domain := range domains {
if routerHTTPConfig.TLS != nil {
tlsOptionsName := routerHTTPConfig.TLS.Options
if tlsOptionsName != defaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
}
tlsConf, err := m.tlsManager.Get(defaultTLSStoreName, tlsOptionsName)
if err != nil {
routerHTTPConfig.AddError(err, true)
logger.Debug(err)
continue
}
tlsConf, err := m.tlsManager.Get(defaultTLSStoreName, tlsOptionsName)
if err != nil {
routerHTTPConfig.AddError(err, true)
logger.Debug(err)
continue
}
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
}
tlsOptionsForHostSNI[domain][tlsOptionsName] = nameAndConfig{
routerName: routerHTTPName,
TLSConfig: tlsConf,
}
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
}
tlsOptionsForHostSNI[domain][routerHTTPConfig.TLS.Options] = nameAndConfig{
routerName: routerHTTPName,
TLSConfig: tlsConf,
}
if _, ok := tlsOptionsForHost[domain]; ok {
// Multiple tlsOptions fallback to default
tlsOptionsForHost[domain] = "default"
} else {
tlsOptionsForHost[domain] = routerHTTPConfig.TLS.Options
}
if name, ok := tlsOptionsForHost[domain]; ok && name != tlsOptionsName {
// Different tlsOptions on the same domain fallback to default
tlsOptionsForHost[domain] = defaultTLSConfigName
} else {
tlsOptionsForHost[domain] = tlsOptionsName
}
}
}
@ -304,5 +302,5 @@ func findTLSOptionName(tlsOptionsForHost map[string]string, host string) string
return tlsOptions
}
return "default"
return defaultTLSConfigName
}

View file

@ -2,25 +2,31 @@ package tcp
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/server/service/tcp"
"github.com/traefik/traefik/v2/pkg/tls"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
)
func TestRuntimeConfiguration(t *testing.T) {
testCases := []struct {
desc string
serviceConfig map[string]*runtime.TCPServiceInfo
routerConfig map[string]*runtime.TCPRouterInfo
expectedError int
desc string
httpServiceConfig map[string]*runtime.ServiceInfo
httpRouterConfig map[string]*runtime.RouterInfo
tcpServiceConfig map[string]*runtime.TCPServiceInfo
tcpRouterConfig map[string]*runtime.TCPRouterInfo
expectedError int
}{
{
desc: "No error",
serviceConfig: map[string]*runtime.TCPServiceInfo{
tcpServiceConfig: map[string]*runtime.TCPServiceInfo{
"foo-service": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
@ -38,7 +44,7 @@ func TestRuntimeConfiguration(t *testing.T) {
},
},
},
routerConfig: map[string]*runtime.TCPRouterInfo{
tcpRouterConfig: map[string]*runtime.TCPRouterInfo{
"foo": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
@ -65,9 +71,54 @@ func TestRuntimeConfiguration(t *testing.T) {
},
expectedError: 0,
},
{
desc: "HTTP routers with same domain but different TLS options",
httpServiceConfig: map[string]*runtime.ServiceInfo{
"foo-service": {
Service: &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
Port: "8085",
URL: "127.0.0.1:8085",
},
{
URL: "127.0.0.1:8086",
Port: "8086",
},
},
},
},
},
},
httpRouterConfig: map[string]*runtime.RouterInfo{
"foo": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`bar.foo`)",
TLS: &dynamic.RouterTLSConfig{
Options: "foo",
},
},
},
"bar": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`bar.foo`) && PathPrefix(`/path`)",
TLS: &dynamic.RouterTLSConfig{
Options: "bar",
},
},
},
},
expectedError: 2,
},
{
desc: "One router with wrong rule",
serviceConfig: map[string]*runtime.TCPServiceInfo{
tcpServiceConfig: map[string]*runtime.TCPServiceInfo{
"foo-service": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
@ -80,7 +131,7 @@ func TestRuntimeConfiguration(t *testing.T) {
},
},
},
routerConfig: map[string]*runtime.TCPRouterInfo{
tcpRouterConfig: map[string]*runtime.TCPRouterInfo{
"foo": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
@ -101,7 +152,7 @@ func TestRuntimeConfiguration(t *testing.T) {
},
{
desc: "All router with wrong rule",
serviceConfig: map[string]*runtime.TCPServiceInfo{
tcpServiceConfig: map[string]*runtime.TCPServiceInfo{
"foo-service": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
@ -114,7 +165,7 @@ func TestRuntimeConfiguration(t *testing.T) {
},
},
},
routerConfig: map[string]*runtime.TCPRouterInfo{
tcpRouterConfig: map[string]*runtime.TCPRouterInfo{
"foo": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
@ -134,7 +185,7 @@ func TestRuntimeConfiguration(t *testing.T) {
},
{
desc: "Router with unknown service",
serviceConfig: map[string]*runtime.TCPServiceInfo{
tcpServiceConfig: map[string]*runtime.TCPServiceInfo{
"foo-service": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
@ -147,7 +198,7 @@ func TestRuntimeConfiguration(t *testing.T) {
},
},
},
routerConfig: map[string]*runtime.TCPRouterInfo{
tcpRouterConfig: map[string]*runtime.TCPRouterInfo{
"foo": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
@ -168,14 +219,14 @@ func TestRuntimeConfiguration(t *testing.T) {
},
{
desc: "Router with broken service",
serviceConfig: map[string]*runtime.TCPServiceInfo{
tcpServiceConfig: map[string]*runtime.TCPServiceInfo{
"foo-service": {
TCPService: &dynamic.TCPService{
LoadBalancer: nil,
},
},
},
routerConfig: map[string]*runtime.TCPRouterInfo{
tcpRouterConfig: map[string]*runtime.TCPRouterInfo{
"bar": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
@ -197,15 +248,17 @@ func TestRuntimeConfiguration(t *testing.T) {
entryPoints := []string{"web"}
conf := &runtime.Configuration{
TCPServices: test.serviceConfig,
TCPRouters: test.routerConfig,
Services: test.httpServiceConfig,
Routers: test.httpRouterConfig,
TCPServices: test.tcpServiceConfig,
TCPRouters: test.tcpRouterConfig,
}
serviceManager := tcp.NewManager(conf)
tlsManager := tls.NewManager()
tlsManager := traefiktls.NewManager()
tlsManager.UpdateConfigs(
context.Background(),
map[string]tls.Store{},
map[string]tls.Options{
map[string]traefiktls.Store{},
map[string]traefiktls.Options{
"default": {
MinVersion: "VersionTLS10",
},
@ -216,7 +269,7 @@ func TestRuntimeConfiguration(t *testing.T) {
MinVersion: "VersionTLS11",
},
},
[]*tls.CertAndStores{})
[]*traefiktls.CertAndStores{})
routerManager := NewManager(conf, serviceManager,
nil, nil, tlsManager)
@ -237,7 +290,235 @@ func TestRuntimeConfiguration(t *testing.T) {
allErrors++
}
}
for _, v := range conf.Services {
if v.Err != nil {
allErrors++
}
}
for _, v := range conf.Routers {
if len(v.Err) > 0 {
allErrors++
}
}
assert.Equal(t, test.expectedError, allErrors)
})
}
}
func TestDomainFronting(t *testing.T) {
tests := []struct {
desc string
routers map[string]*runtime.RouterInfo
expectedStatus int
}{
{
desc: "Request is misdirected when TLS options are different",
routers: map[string]*runtime.RouterInfo{
"router-1@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host1.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
"router-2@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host2.local`)",
TLS: &dynamic.RouterTLSConfig{},
},
},
},
expectedStatus: http.StatusMisdirectedRequest,
},
{
desc: "Request is OK when TLS options are the same",
routers: map[string]*runtime.RouterInfo{
"router-1@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host1.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
"router-2@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host2.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
},
expectedStatus: http.StatusOK,
},
{
desc: "Default TLS options is used when options are ambiguous for the same host",
routers: map[string]*runtime.RouterInfo{
"router-1@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host1.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
"router-2@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host1.local`) && PathPrefix(`/foo`)",
TLS: &dynamic.RouterTLSConfig{
Options: "default",
},
},
},
"router-3@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host2.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
},
expectedStatus: http.StatusMisdirectedRequest,
},
{
desc: "Default TLS options should not be used when options are the same for the same host",
routers: map[string]*runtime.RouterInfo{
"router-1@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host1.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
"router-2@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host1.local`) && PathPrefix(`/bar`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
"router-3@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host2.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
},
expectedStatus: http.StatusOK,
},
{
desc: "Request is misdirected when TLS options have the same name but from different providers",
routers: map[string]*runtime.RouterInfo{
"router-1@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host1.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
"router-2@crd": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host2.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1",
},
},
},
},
expectedStatus: http.StatusMisdirectedRequest,
},
{
desc: "Request is OK when TLS options reference from a different provider is the same",
routers: map[string]*runtime.RouterInfo{
"router-1@file": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host1.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1@crd",
},
},
},
"router-2@crd": {
Router: &dynamic.Router{
EntryPoints: []string{"web"},
Rule: "Host(`host2.local`)",
TLS: &dynamic.RouterTLSConfig{
Options: "host1@crd",
},
},
},
},
expectedStatus: http.StatusOK,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
entryPoints := []string{"web"}
tlsOptions := map[string]traefiktls.Options{
"default": {
MinVersion: "VersionTLS10",
},
"host1@file": {
MinVersion: "VersionTLS12",
},
"host1@crd": {
MinVersion: "VersionTLS12",
},
}
conf := &runtime.Configuration{
Routers: test.routers,
}
serviceManager := tcp.NewManager(conf)
tlsManager := traefiktls.NewManager()
tlsManager.UpdateConfigs(context.Background(), map[string]traefiktls.Store{}, tlsOptions, []*traefiktls.CertAndStores{})
httpsHandler := map[string]http.Handler{
"web": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}),
}
routerManager := NewManager(conf, serviceManager, nil, httpsHandler, tlsManager)
routers := routerManager.BuildHandlers(context.Background(), entryPoints)
router, ok := routers["web"]
require.True(t, ok)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Host = "host1.local"
req.TLS = &tls.ConnectionState{
ServerName: "host2.local",
}
rw := httptest.NewRecorder()
router.GetHTTPSHandler().ServeHTTP(rw, req)
assert.Equal(t, test.expectedStatus, rw.Code)
})
}
}

View file

@ -4,11 +4,11 @@ RepositoryName = "traefik"
OutputType = "file"
FileName = "traefik_changelog.md"
# example new bugfix v2.3.3
# example new bugfix v2.3.5
CurrentRef = "v2.3"
PreviousRef = "v2.3.2"
PreviousRef = "v2.3.4"
BaseBranch = "v2.3"
FutureCurrentRefName = "v2.3.3"
FutureCurrentRefName = "v2.3.5"
ThresholdPreviousRef = 10
ThresholdCurrentRef = 10

View file

@ -231,8 +231,9 @@
<div class="text-subtitle2">Service</div>
<q-chip
dense
class="app-chip app-chip-green">
class="app-chip app-chip-green app-chip-overflow">
{{ exData(middleware).service }}
<q-tooltip>{{ exData(middleware).service }}</q-tooltip>
</q-chip>
</div>
</div>

View file

@ -21,8 +21,9 @@
<div class="col-6">
<q-chip
dense
class="app-chip app-chip-rule">
class="app-chip app-chip-rule app-chip-overflow">
{{ service.name }}
<q-tooltip>{{service.name}}</q-tooltip>
</q-chip>
</div>
<div class="col-3 text-right">

View file

@ -66,8 +66,9 @@
dense
clickable
@click.native="$router.push({ path: `/${protocol}/services/${getServiceId()}`})"
class="app-chip app-chip-wrap app-chip-service">
class="app-chip app-chip-wrap app-chip-service app-chip-overflow">
{{ data.service }}
<q-tooltip>{{ data.service }}</q-tooltip>
</q-chip>
</div>
</div>

View file

@ -21,8 +21,9 @@
<div class="col-7">
<q-chip
dense
class="app-chip app-chip-rule">
class="app-chip app-chip-rule app-chip-overflow">
{{ service.name }}
<q-tooltip>{{service.name}}</q-tooltip>
</q-chip>
</div>
<div class="col-3">

View file

@ -131,6 +131,16 @@ body {
white-space: normal;
}
}
&-overflow {
max-width: 90%;
.q-chip__content{
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&-accent, &-rule {
color: $accent;
background-color: rgba($accent, 0.1);