Merge current v2.4 into v2.5
This commit is contained in:
commit
3a180e2afc
28 changed files with 847 additions and 166 deletions
9
Makefile
9
Makefile
|
@ -96,6 +96,15 @@ test-integration: $(PRE_TARGET)
|
||||||
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK),TEST_CONTAINER=1) ./script/make.sh generate binary test-integration
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK),TEST_CONTAINER=1) ./script/make.sh generate binary test-integration
|
||||||
TEST_HOST=1 ./script/make.sh test-integration
|
TEST_HOST=1 ./script/make.sh test-integration
|
||||||
|
|
||||||
|
## Run the container integration tests
|
||||||
|
test-integration-container: $(PRE_TARGET)
|
||||||
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK),TEST_CONTAINER=1) ./script/make.sh generate binary test-integration
|
||||||
|
|
||||||
|
## Run the host integration tests
|
||||||
|
test-integration-host: $(PRE_TARGET)
|
||||||
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK),TEST_CONTAINER=1) ./script/make.sh generate binary
|
||||||
|
TEST_HOST=1 ./script/make.sh test-integration
|
||||||
|
|
||||||
## Validate code and docs
|
## Validate code and docs
|
||||||
validate-files: $(PRE_TARGET)
|
validate-files: $(PRE_TARGET)
|
||||||
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK)) ./script/make.sh generate validate-lint validate-misspell
|
$(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK)) ./script/make.sh generate validate-lint validate-misspell
|
||||||
|
|
|
@ -124,3 +124,16 @@ http:
|
||||||
If there is a need for a response code other than a `503` and/or a custom message,
|
If there is a need for a response code other than a `503` and/or a custom message,
|
||||||
the principle of the above example above (a catchall router) still stands,
|
the principle of the above example above (a catchall router) still stands,
|
||||||
but the `unavailable` service should be adapted to fit such a need.
|
but the `unavailable` service should be adapted to fit such a need.
|
||||||
|
|
||||||
|
## Why Is My TLS Certificate Not Reloaded When Its Contents Change ?
|
||||||
|
|
||||||
|
With the file provider,
|
||||||
|
a configuration update is only triggered when one of the [watched](../providers/file.md#provider-configuration) configuration files is modified.
|
||||||
|
|
||||||
|
Which is why, when a certificate is defined by path,
|
||||||
|
and the actual contents of this certificate change,
|
||||||
|
a configuration update is _not_ triggered.
|
||||||
|
|
||||||
|
To take into account the new certificate contents, the update of the dynamic configuration must be forced.
|
||||||
|
One way to achieve that, is to trigger a file notification,
|
||||||
|
for example, by using the `touch` command on the configuration file.
|
||||||
|
|
|
@ -365,6 +365,17 @@ For more information, please read the [HTTP routers rule](../routing/routers/ind
|
||||||
|
|
||||||
In `v2.4.9`, we changed span error to log only server errors (>= 500).
|
In `v2.4.9`, we changed span error to log only server errors (>= 500).
|
||||||
|
|
||||||
|
## v2.4.9 to v2.4.10
|
||||||
|
|
||||||
|
### K8S CrossNamespace
|
||||||
|
|
||||||
|
In `v2.4.10`, the default value for `allowCrossNamespace` has been changed to `false`.
|
||||||
|
|
||||||
|
### K8S ExternalName Service
|
||||||
|
|
||||||
|
In `v2.4.10`, by default, it is no longer authorized to reference Kubernetes ExternalName services.
|
||||||
|
To allow it, the `allowExternalNameServices` option should be set to `true`.
|
||||||
|
|
||||||
## v2.4 to v2.5
|
## v2.4 to v2.5
|
||||||
|
|
||||||
### Kubernetes CRD
|
### Kubernetes CRD
|
||||||
|
|
|
@ -266,29 +266,48 @@ providers:
|
||||||
|
|
||||||
### `allowCrossNamespace`
|
### `allowCrossNamespace`
|
||||||
|
|
||||||
_Optional, Default: true_
|
_Optional, Default: false_
|
||||||
|
|
||||||
If the parameter is set to `false`, IngressRoutes are not able to reference any resources in other namespaces than theirs.
|
If the parameter is set to `true`, IngressRoutes are able to reference resources in other namespaces than theirs.
|
||||||
|
|
||||||
!!! warning "Deprecation"
|
|
||||||
|
|
||||||
Please note that the default value for this option will be set to `false` in a future version.
|
|
||||||
|
|
||||||
```yaml tab="File (YAML)"
|
```yaml tab="File (YAML)"
|
||||||
providers:
|
providers:
|
||||||
kubernetesCRD:
|
kubernetesCRD:
|
||||||
allowCrossNamespace: false
|
allowCrossNamespace: true
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
[providers.kubernetesCRD]
|
[providers.kubernetesCRD]
|
||||||
allowCrossNamespace = false
|
allowCrossNamespace = true
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash tab="CLI"
|
```bash tab="CLI"
|
||||||
--providers.kubernetescrd.allowCrossNamespace=false
|
--providers.kubernetescrd.allowCrossNamespace=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `allowExternalNameServices`
|
||||||
|
|
||||||
|
_Optional, Default: false_
|
||||||
|
|
||||||
|
If the parameter is set to `true`, IngressRoutes are able to reference ExternalName services.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
kubernetesCRD:
|
||||||
|
allowExternalNameServices: true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.kubernetesCRD]
|
||||||
|
allowExternalNameServices = true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.kubernetescrd.allowexternalnameservices=true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Full Example
|
## Full Example
|
||||||
|
|
|
@ -464,6 +464,29 @@ providers:
|
||||||
Allow the creation of services if there are no endpoints available.
|
Allow the creation of services if there are no endpoints available.
|
||||||
This results in `503` http responses instead of `404`.
|
This results in `503` http responses instead of `404`.
|
||||||
|
|
||||||
|
### `allowExternalNameServices`
|
||||||
|
|
||||||
|
_Optional, Default: false_
|
||||||
|
|
||||||
|
If the parameter is set to `true`, Ingresses are able to reference ExternalName services.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
kubernetesIngress:
|
||||||
|
allowExternalNameServices: true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.kubernetesIngress]
|
||||||
|
allowExternalNameServices = true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.kubernetesingress.allowexternalnameservices=true
|
||||||
|
```
|
||||||
|
|
||||||
### Further
|
### Further
|
||||||
|
|
||||||
To learn more about the various aspects of the Ingress specification that Traefik supports,
|
To learn more about the various aspects of the Ingress specification that Traefik supports,
|
||||||
|
|
|
@ -577,7 +577,10 @@ TLS key
|
||||||
Enable Kubernetes backend with default settings. (Default: ```false```)
|
Enable Kubernetes backend with default settings. (Default: ```false```)
|
||||||
|
|
||||||
`--providers.kubernetescrd.allowcrossnamespace`:
|
`--providers.kubernetescrd.allowcrossnamespace`:
|
||||||
Allow cross namespace resource reference. (Default: ```true```)
|
Allow cross namespace resource reference. (Default: ```false```)
|
||||||
|
|
||||||
|
`--providers.kubernetescrd.allowexternalnameservices`:
|
||||||
|
Allow ExternalName services. (Default: ```false```)
|
||||||
|
|
||||||
`--providers.kubernetescrd.certauthfilepath`:
|
`--providers.kubernetescrd.certauthfilepath`:
|
||||||
Kubernetes certificate authority file path (not needed for in-cluster client).
|
Kubernetes certificate authority file path (not needed for in-cluster client).
|
||||||
|
@ -627,6 +630,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```)
|
||||||
`--providers.kubernetesingress.allowemptyservices`:
|
`--providers.kubernetesingress.allowemptyservices`:
|
||||||
Allow creation of services without endpoints. (Default: ```false```)
|
Allow creation of services without endpoints. (Default: ```false```)
|
||||||
|
|
||||||
|
`--providers.kubernetesingress.allowexternalnameservices`:
|
||||||
|
Allow ExternalName services. (Default: ```false```)
|
||||||
|
|
||||||
`--providers.kubernetesingress.certauthfilepath`:
|
`--providers.kubernetesingress.certauthfilepath`:
|
||||||
Kubernetes certificate authority file path (not needed for in-cluster client).
|
Kubernetes certificate authority file path (not needed for in-cluster client).
|
||||||
|
|
||||||
|
|
|
@ -577,7 +577,10 @@ TLS key
|
||||||
Enable Kubernetes backend with default settings. (Default: ```false```)
|
Enable Kubernetes backend with default settings. (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWCROSSNAMESPACE`:
|
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWCROSSNAMESPACE`:
|
||||||
Allow cross namespace resource reference. (Default: ```true```)
|
Allow cross namespace resource reference. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWEXTERNALNAMESERVICES`:
|
||||||
|
Allow ExternalName services. (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`:
|
`TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`:
|
||||||
Kubernetes certificate authority file path (not needed for in-cluster client).
|
Kubernetes certificate authority file path (not needed for in-cluster client).
|
||||||
|
@ -627,6 +630,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```)
|
||||||
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ALLOWEMPTYSERVICES`:
|
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ALLOWEMPTYSERVICES`:
|
||||||
Allow creation of services without endpoints. (Default: ```false```)
|
Allow creation of services without endpoints. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ALLOWEXTERNALNAMESERVICES`:
|
||||||
|
Allow ExternalName services. (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
|
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
|
||||||
Kubernetes certificate authority file path (not needed for in-cluster client).
|
Kubernetes certificate authority file path (not needed for in-cluster client).
|
||||||
|
|
||||||
|
|
|
@ -17,3 +17,4 @@
|
||||||
|
|
||||||
[providers.kubernetesCRD]
|
[providers.kubernetesCRD]
|
||||||
allowCrossNamespace = false
|
allowCrossNamespace = false
|
||||||
|
allowExternalNameServices = true
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
@ -347,7 +348,7 @@ func (h *Handler) redactHeaders(headers http.Header, fields logrus.Fields, prefi
|
||||||
for k := range headers {
|
for k := range headers {
|
||||||
v := h.config.Fields.KeepHeader(k)
|
v := h.config.Fields.KeepHeader(k)
|
||||||
if v == types.AccessLogKeep {
|
if v == types.AccessLogKeep {
|
||||||
fields[prefix+k] = headers.Get(k)
|
fields[prefix+k] = strings.Join(headers.Values(k), ",")
|
||||||
} else if v == types.AccessLogRedact {
|
} else if v == types.AccessLogRedact {
|
||||||
fields[prefix+k] = "REDACTED"
|
fields[prefix+k] = "REDACTED"
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,7 @@ func lineCount(t *testing.T, fileName string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerHeaderFields(t *testing.T) {
|
func TestLoggerHeaderFields(t *testing.T) {
|
||||||
expectedValue := "expectedValue"
|
expectedValues := []string{"AAA", "BBB"}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
|
@ -191,7 +191,10 @@ func TestLoggerHeaderFields(t *testing.T) {
|
||||||
Path: testPath,
|
Path: testPath,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
req.Header.Set(test.header, expectedValue)
|
|
||||||
|
for _, s := range expectedValues {
|
||||||
|
req.Header.Add(test.header, s)
|
||||||
|
}
|
||||||
|
|
||||||
logger.ServeHTTP(httptest.NewRecorder(), req, http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) {
|
logger.ServeHTTP(httptest.NewRecorder(), req, http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) {
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
@ -201,9 +204,9 @@ func TestLoggerHeaderFields(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if test.expected == types.AccessLogDrop {
|
if test.expected == types.AccessLogDrop {
|
||||||
assert.NotContains(t, string(logData), expectedValue)
|
assert.NotContains(t, string(logData), strings.Join(expectedValues, ","))
|
||||||
} else {
|
} else {
|
||||||
assert.Contains(t, string(logData), expectedValue)
|
assert.Contains(t, string(logData), strings.Join(expectedValues, ","))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type responseModifier struct {
|
type responseModifier struct {
|
||||||
r *http.Request
|
req *http.Request
|
||||||
w http.ResponseWriter
|
rw http.ResponseWriter
|
||||||
|
|
||||||
headersSent bool // whether headers have already been sent
|
headersSent bool // whether headers have already been sent
|
||||||
code int // status code, must default to 200
|
code int // status code, must default to 200
|
||||||
|
@ -24,71 +24,76 @@ type responseModifier struct {
|
||||||
// modifier can be nil.
|
// modifier can be nil.
|
||||||
func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) *responseModifier {
|
func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) *responseModifier {
|
||||||
return &responseModifier{
|
return &responseModifier{
|
||||||
r: r,
|
req: r,
|
||||||
w: w,
|
rw: w,
|
||||||
modifier: modifier,
|
modifier: modifier,
|
||||||
code: http.StatusOK,
|
code: http.StatusOK,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseModifier) WriteHeader(code int) {
|
func (r *responseModifier) WriteHeader(code int) {
|
||||||
if w.headersSent {
|
if r.headersSent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
w.code = code
|
r.code = code
|
||||||
w.headersSent = true
|
r.headersSent = true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if w.modifier == nil || w.modified {
|
if r.modifier == nil || r.modified {
|
||||||
w.w.WriteHeader(code)
|
r.rw.WriteHeader(code)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := http.Response{
|
resp := http.Response{
|
||||||
Header: w.w.Header(),
|
Header: r.rw.Header(),
|
||||||
Request: w.r,
|
Request: r.req,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.modifier(&resp); err != nil {
|
if err := r.modifier(&resp); err != nil {
|
||||||
w.modifierErr = err
|
r.modifierErr = err
|
||||||
// we are propagating when we are called in Write, but we're logging anyway,
|
// we are propagating when we are called in Write, but we're logging anyway,
|
||||||
// because we could be called from another place which does not take care of
|
// because we could be called from another place which does not take care of
|
||||||
// checking w.modifierErr.
|
// checking w.modifierErr.
|
||||||
log.WithoutContext().Errorf("Error when applying response modifier: %v", err)
|
log.WithoutContext().Errorf("Error when applying response modifier: %v", err)
|
||||||
w.w.WriteHeader(http.StatusInternalServerError)
|
r.rw.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.modified = true
|
r.modified = true
|
||||||
w.w.WriteHeader(code)
|
r.rw.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseModifier) Header() http.Header {
|
func (r *responseModifier) Header() http.Header {
|
||||||
return w.w.Header()
|
return r.rw.Header()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseModifier) Write(b []byte) (int, error) {
|
func (r *responseModifier) Write(b []byte) (int, error) {
|
||||||
w.WriteHeader(w.code)
|
r.WriteHeader(r.code)
|
||||||
if w.modifierErr != nil {
|
if r.modifierErr != nil {
|
||||||
return 0, w.modifierErr
|
return 0, r.modifierErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.w.Write(b)
|
return r.rw.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack hijacks the connection.
|
// Hijack hijacks the connection.
|
||||||
func (w *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
func (r *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
if h, ok := w.w.(http.Hijacker); ok {
|
if h, ok := r.rw.(http.Hijacker); ok {
|
||||||
return h.Hijack()
|
return h.Hijack()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, fmt.Errorf("not a hijacker: %T", w.w)
|
return nil, nil, fmt.Errorf("not a hijacker: %T", r.rw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush sends any buffered data to the client.
|
// Flush sends any buffered data to the client.
|
||||||
func (w *responseModifier) Flush() {
|
func (r *responseModifier) Flush() {
|
||||||
if flusher, ok := w.w.(http.Flusher); ok {
|
if flusher, ok := r.rw.(http.Flusher); ok {
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseNotify implements http.CloseNotifier.
|
||||||
|
func (r *responseModifier) CloseNotify() <-chan bool {
|
||||||
|
return r.rw.(http.CloseNotifier).CloseNotify()
|
||||||
|
}
|
||||||
|
|
|
@ -167,6 +167,86 @@ func (p *Provider) loadFileConfig(ctx context.Context, filename string, parseTem
|
||||||
|
|
||||||
if configuration.TLS != nil {
|
if configuration.TLS != nil {
|
||||||
configuration.TLS.Certificates = flattenCertificates(ctx, configuration.TLS)
|
configuration.TLS.Certificates = flattenCertificates(ctx, configuration.TLS)
|
||||||
|
|
||||||
|
// TLS Options
|
||||||
|
if configuration.TLS.Options != nil {
|
||||||
|
for name, options := range configuration.TLS.Options {
|
||||||
|
var caCerts []tls.FileOrContent
|
||||||
|
|
||||||
|
for _, caFile := range options.ClientAuth.CAFiles {
|
||||||
|
content, err := caFile.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
caCerts = append(caCerts, tls.FileOrContent(content))
|
||||||
|
}
|
||||||
|
options.ClientAuth.CAFiles = caCerts
|
||||||
|
|
||||||
|
configuration.TLS.Options[name] = options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS stores
|
||||||
|
if len(configuration.TLS.Stores) > 0 {
|
||||||
|
for name, store := range configuration.TLS.Stores {
|
||||||
|
content, err := store.DefaultCertificate.CertFile.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
store.DefaultCertificate.CertFile = tls.FileOrContent(content)
|
||||||
|
|
||||||
|
content, err = store.DefaultCertificate.KeyFile.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
store.DefaultCertificate.KeyFile = tls.FileOrContent(content)
|
||||||
|
|
||||||
|
configuration.TLS.Stores[name] = store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServersTransport
|
||||||
|
if configuration.HTTP != nil && len(configuration.HTTP.ServersTransports) > 0 {
|
||||||
|
for name, st := range configuration.HTTP.ServersTransports {
|
||||||
|
var certificates []tls.Certificate
|
||||||
|
for _, cert := range st.Certificates {
|
||||||
|
content, err := cert.CertFile.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cert.CertFile = tls.FileOrContent(content)
|
||||||
|
|
||||||
|
content, err = cert.KeyFile.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cert.KeyFile = tls.FileOrContent(content)
|
||||||
|
|
||||||
|
certificates = append(certificates, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration.HTTP.ServersTransports[name].Certificates = certificates
|
||||||
|
|
||||||
|
var rootCAs []tls.FileOrContent
|
||||||
|
for _, rootCA := range st.RootCAs {
|
||||||
|
content, err := rootCA.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCAs = append(rootCAs, tls.FileOrContent(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
st.RootCAs = rootCAs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return configuration, nil
|
return configuration, nil
|
||||||
|
|
|
@ -25,19 +25,35 @@ type ProvideTestCase struct {
|
||||||
expectedNumTLSOptions int
|
expectedNumTLSOptions int
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSContent(t *testing.T) {
|
func TestTLSCertificateContent(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
fileTLS, err := createTempFile("./fixtures/toml/tls_file.cert", tempDir)
|
fileTLS, err := createTempFile("./fixtures/toml/tls_file.cert", tempDir)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fileTLSKey, err := createTempFile("./fixtures/toml/tls_file_key.cert", tempDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
fileConfig, err := os.CreateTemp(tempDir, "temp*.toml")
|
fileConfig, err := os.CreateTemp(tempDir, "temp*.toml")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
content := `
|
content := `
|
||||||
[[tls.certificates]]
|
[[tls.certificates]]
|
||||||
certFile = "` + fileTLS.Name() + `"
|
certFile = "` + fileTLS.Name() + `"
|
||||||
keyFile = "` + fileTLS.Name() + `"
|
keyFile = "` + fileTLSKey.Name() + `"
|
||||||
|
|
||||||
|
[tls.options.default.clientAuth]
|
||||||
|
caFiles = ["` + fileTLS.Name() + `"]
|
||||||
|
|
||||||
|
[tls.stores.default.defaultCertificate]
|
||||||
|
certFile = "` + fileTLS.Name() + `"
|
||||||
|
keyFile = "` + fileTLSKey.Name() + `"
|
||||||
|
|
||||||
|
[http.serversTransports.default]
|
||||||
|
rootCAs = ["` + fileTLS.Name() + `"]
|
||||||
|
[[http.serversTransports.default.certificates]]
|
||||||
|
certFile = "` + fileTLS.Name() + `"
|
||||||
|
keyFile = "` + fileTLSKey.Name() + `"
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err = fileConfig.Write([]byte(content))
|
_, err = fileConfig.Write([]byte(content))
|
||||||
|
@ -48,7 +64,16 @@ func TestTLSContent(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, "CONTENT", configuration.TLS.Certificates[0].Certificate.CertFile.String())
|
require.Equal(t, "CONTENT", configuration.TLS.Certificates[0].Certificate.CertFile.String())
|
||||||
require.Equal(t, "CONTENT", configuration.TLS.Certificates[0].Certificate.KeyFile.String())
|
require.Equal(t, "CONTENTKEY", configuration.TLS.Certificates[0].Certificate.KeyFile.String())
|
||||||
|
|
||||||
|
require.Equal(t, "CONTENT", configuration.TLS.Options["default"].ClientAuth.CAFiles[0].String())
|
||||||
|
|
||||||
|
require.Equal(t, "CONTENT", configuration.TLS.Stores["default"].DefaultCertificate.CertFile.String())
|
||||||
|
require.Equal(t, "CONTENTKEY", configuration.TLS.Stores["default"].DefaultCertificate.KeyFile.String())
|
||||||
|
|
||||||
|
require.Equal(t, "CONTENT", configuration.HTTP.ServersTransports["default"].Certificates[0].CertFile.String())
|
||||||
|
require.Equal(t, "CONTENTKEY", configuration.HTTP.ServersTransports["default"].Certificates[0].KeyFile.String())
|
||||||
|
require.Equal(t, "CONTENT", configuration.HTTP.ServersTransports["default"].RootCAs[0].String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorWhenEmptyConfig(t *testing.T) {
|
func TestErrorWhenEmptyConfig(t *testing.T) {
|
||||||
|
|
1
pkg/provider/file/fixtures/toml/tls_file_key.cert
Normal file
1
pkg/provider/file/fixtures/toml/tls_file_key.cert
Normal file
|
@ -0,0 +1 @@
|
||||||
|
CONTENTKEY
|
|
@ -0,0 +1,14 @@
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRouteUDP
|
||||||
|
metadata:
|
||||||
|
name: test.route
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- foo
|
||||||
|
|
||||||
|
routes:
|
||||||
|
- services:
|
||||||
|
- name: external.service.with.port
|
||||||
|
port: 80
|
|
@ -41,20 +41,16 @@ const (
|
||||||
|
|
||||||
// Provider holds configurations of the provider.
|
// Provider holds configurations of the provider.
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||||
Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
|
Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
|
||||||
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
|
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
|
||||||
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
|
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"`
|
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"`
|
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,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"`
|
LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
|
||||||
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,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"`
|
||||||
lastConfiguration safe.Safe
|
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) {
|
func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
|
||||||
|
@ -106,10 +102,14 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.AllowCrossNamespace == nil || *p.AllowCrossNamespace {
|
if p.AllowCrossNamespace {
|
||||||
logger.Warn("Cross-namespace reference between IngressRoutes and resources is enabled, please ensure that this is expected (see AllowCrossNamespace option)")
|
logger.Warn("Cross-namespace reference between IngressRoutes and resources is enabled, please ensure that this is expected (see AllowCrossNamespace option)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.AllowExternalNameServices {
|
||||||
|
logger.Warn("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
|
||||||
|
}
|
||||||
|
|
||||||
pool.GoCtx(func(ctxPool context.Context) {
|
pool.GoCtx(func(ctxPool context.Context) {
|
||||||
operation := func() error {
|
operation := func() error {
|
||||||
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
|
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
|
||||||
|
@ -274,7 +274,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cb := configBuilder{client, p.AllowCrossNamespace}
|
cb := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices}
|
||||||
|
|
||||||
for _, service := range client.GetTraefikServices() {
|
for _, service := range client.GetTraefikServices() {
|
||||||
err := cb.buildTraefikService(ctx, service, conf.HTTP.Services)
|
err := cb.buildTraefikService(ctx, service, conf.HTTP.Services)
|
||||||
|
@ -450,7 +450,7 @@ func (p *Provider) createErrorPageMiddleware(client Client, namespace string, er
|
||||||
Query: errorPage.Query,
|
Query: errorPage.Query,
|
||||||
}
|
}
|
||||||
|
|
||||||
balancerServerHTTP, err := configBuilder{client, p.AllowCrossNamespace}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec)
|
balancerServerHTTP, err := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -919,7 +919,7 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *s
|
||||||
return eventsChanBuffered
|
return eventsChanBuffered
|
||||||
}
|
}
|
||||||
|
|
||||||
func isNamespaceAllowed(allowCrossNamespace *bool, parentNamespace, namespace string) bool {
|
func isNamespaceAllowed(allowCrossNamespace bool, parentNamespace, namespace string) bool {
|
||||||
// If allowCrossNamespace option is not defined the default behavior is to allow cross namespace references.
|
// If allowCrossNamespace option is not defined the default behavior is to allow cross namespace references.
|
||||||
return allowCrossNamespace == nil || *allowCrossNamespace || parentNamespace == namespace
|
return allowCrossNamespace || parentNamespace == namespace
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
|
||||||
ingressName = ingressRoute.GenerateName
|
ingressName = ingressRoute.GenerateName
|
||||||
}
|
}
|
||||||
|
|
||||||
cb := configBuilder{client, p.AllowCrossNamespace}
|
cb := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices}
|
||||||
|
|
||||||
for _, route := range ingressRoute.Spec.Routes {
|
for _, route := range ingressRoute.Spec.Routes {
|
||||||
if route.Kind != "Rule" {
|
if route.Kind != "Rule" {
|
||||||
|
@ -173,8 +173,9 @@ func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace str
|
||||||
}
|
}
|
||||||
|
|
||||||
type configBuilder struct {
|
type configBuilder struct {
|
||||||
client Client
|
client Client
|
||||||
allowCrossNamespace *bool
|
allowCrossNamespace bool
|
||||||
|
allowExternalNameServices bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildTraefikService creates the configuration for the traefik service defined in tService,
|
// buildTraefikService creates the configuration for the traefik service defined in tService,
|
||||||
|
@ -323,6 +324,10 @@ func (c configBuilder) loadServers(parentNamespace string, svc v1alpha1.LoadBala
|
||||||
|
|
||||||
var servers []dynamic.Server
|
var servers []dynamic.Server
|
||||||
if service.Spec.Type == corev1.ServiceTypeExternalName {
|
if service.Spec.Type == corev1.ServiceTypeExternalName {
|
||||||
|
if !c.allowExternalNameServices {
|
||||||
|
return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, sanitizedName)
|
||||||
|
}
|
||||||
|
|
||||||
protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port)
|
protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -172,7 +172,7 @@ func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace st
|
||||||
ns = service.Namespace
|
ns = service.Namespace
|
||||||
}
|
}
|
||||||
|
|
||||||
servers, err := loadTCPServers(client, ns, service)
|
servers, err := p.loadTCPServers(client, ns, service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -199,7 +199,7 @@ func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace st
|
||||||
return tcpService, nil
|
return tcpService, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) {
|
func (p *Provider) loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) {
|
||||||
service, exists, err := client.GetService(namespace, svc.Name)
|
service, exists, err := client.GetService(namespace, svc.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -209,6 +209,10 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([
|
||||||
return nil, errors.New("service not found")
|
return nil, errors.New("service not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if service.Spec.Type == corev1.ServiceTypeExternalName && !p.AllowExternalNameServices {
|
||||||
|
return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, svc.Name)
|
||||||
|
}
|
||||||
|
|
||||||
svcPort, err := getServicePort(service, svc.Port)
|
svcPort, err := getServicePort(service, svc.Port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1294,8 +1294,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p := Provider{IngressClass: test.ingressClass}
|
p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true}
|
||||||
p.SetDefaults()
|
|
||||||
|
|
||||||
clientMock := newClientMock(test.paths...)
|
clientMock := newClientMock(test.paths...)
|
||||||
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
|
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
|
||||||
|
@ -3476,8 +3475,7 @@ func TestLoadIngressRoutes(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p := Provider{IngressClass: test.ingressClass}
|
p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true}
|
||||||
p.SetDefaults()
|
|
||||||
|
|
||||||
clientMock := newClientMock(test.paths...)
|
clientMock := newClientMock(test.paths...)
|
||||||
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
|
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
|
||||||
|
@ -3894,8 +3892,7 @@ func TestLoadIngressRouteUDPs(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p := Provider{IngressClass: test.ingressClass}
|
p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true}
|
||||||
p.SetDefaults()
|
|
||||||
|
|
||||||
clientMock := newClientMock(test.paths...)
|
clientMock := newClientMock(test.paths...)
|
||||||
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
|
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
|
||||||
|
@ -4895,10 +4892,299 @@ func TestCrossNamespace(t *testing.T) {
|
||||||
<-eventCh
|
<-eventCh
|
||||||
}
|
}
|
||||||
|
|
||||||
p := Provider{}
|
p := Provider{AllowCrossNamespace: test.allowCrossNamespace}
|
||||||
p.SetDefaults()
|
|
||||||
|
conf := p.loadConfigurationFromCRD(context.Background(), client)
|
||||||
|
assert.Equal(t, test.expected, conf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExternalNameService(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
allowExternalNameService 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{},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
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 ExternalName services allowed",
|
||||||
|
paths: []string{"services.yml", "with_externalname_with_http.yml"},
|
||||||
|
allowExternalNameService: 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{},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
Services: map[string]*dynamic.TCPService{},
|
||||||
|
},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"default-test-route-6f97418635c7e18853da": {
|
||||||
|
EntryPoints: []string{"foo"},
|
||||||
|
Service: "default-test-route-6f97418635c7e18853da",
|
||||||
|
Rule: "Host(`foo.com`)",
|
||||||
|
Priority: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"default-test-route-6f97418635c7e18853da": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://external.domain:80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "HTTP Externalname services disallowed",
|
||||||
|
paths: []string{"services.yml", "with_externalname_with_http.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{},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
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: "TCP ExternalName services allowed",
|
||||||
|
paths: []string{"tcp/services.yml", "tcp/with_externalname_with_port.yml"},
|
||||||
|
allowExternalNameService: 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`)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
Services: map[string]*dynamic.TCPService{
|
||||||
|
"default-test.route-fdd3e9338e47a45efefc": {
|
||||||
|
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
||||||
|
Servers: []dynamic.TCPServer{
|
||||||
|
{
|
||||||
|
Address: "external.domain:80",
|
||||||
|
Port: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "TCP ExternalName services disallowed",
|
||||||
|
paths: []string{"tcp/services.yml", "tcp/with_externalname_with_port.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`)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
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 ExternalName services allowed",
|
||||||
|
paths: []string{"udp/services.yml", "udp/with_externalname_service.yml"},
|
||||||
|
allowExternalNameService: 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: "external.domain:80",
|
||||||
|
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{},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
Services: map[string]*dynamic.TCPService{},
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "UDP ExternalName service disallowed",
|
||||||
|
paths: []string{"udp/services.yml", "udp/with_externalname_service.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{},
|
||||||
|
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||||
|
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 := os.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{AllowExternalNameServices: test.allowExternalNameService}
|
||||||
|
|
||||||
p.AllowCrossNamespace = Bool(test.allowCrossNamespace)
|
|
||||||
conf := p.loadConfigurationFromCRD(context.Background(), client)
|
conf := p.loadConfigurationFromCRD(context.Background(), client)
|
||||||
assert.Equal(t, test.expected, conf)
|
assert.Equal(t, test.expected, conf)
|
||||||
})
|
})
|
||||||
|
|
|
@ -87,7 +87,7 @@ func (p *Provider) createLoadBalancerServerUDP(client Client, parentNamespace st
|
||||||
ns = service.Namespace
|
ns = service.Namespace
|
||||||
}
|
}
|
||||||
|
|
||||||
servers, err := loadUDPServers(client, ns, service)
|
servers, err := p.loadUDPServers(client, ns, service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ func (p *Provider) createLoadBalancerServerUDP(client Client, parentNamespace st
|
||||||
return udpService, nil
|
return udpService, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([]dynamic.UDPServer, error) {
|
func (p *Provider) loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([]dynamic.UDPServer, error) {
|
||||||
service, exists, err := client.GetService(namespace, svc.Name)
|
service, exists, err := client.GetService(namespace, svc.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -111,6 +111,10 @@ func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([
|
||||||
return nil, errors.New("service not found")
|
return nil, errors.New("service not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if service.Spec.Type == corev1.ServiceTypeExternalName && !p.AllowExternalNameServices {
|
||||||
|
return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, svc.Name)
|
||||||
|
}
|
||||||
|
|
||||||
svcPort, err := getServicePort(service, svc.Port)
|
svcPort, err := getServicePort(service, svc.Port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: example.com
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /foo
|
||||||
|
backend:
|
||||||
|
serviceName: service-foo
|
||||||
|
servicePort: 8080
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
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"
|
|
@ -0,0 +1,15 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: ""
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: traefik.tchouk
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /bar
|
||||||
|
backend:
|
||||||
|
serviceName: service1
|
||||||
|
servicePort: 8080
|
|
@ -0,0 +1,13 @@
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
clusterIP: 10.0.0.1
|
||||||
|
type: ExternalName
|
||||||
|
externalName: traefik.wtf
|
||||||
|
|
|
@ -36,16 +36,17 @@ const (
|
||||||
|
|
||||||
// Provider holds configurations of the provider.
|
// Provider holds configurations of the provider.
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||||
Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
|
Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
|
||||||
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
|
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
|
||||||
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
|
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
|
||||||
LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
|
LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
|
||||||
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
|
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
|
||||||
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
|
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
|
||||||
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
|
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
|
||||||
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
|
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
|
||||||
lastConfiguration safe.Safe
|
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
|
||||||
|
lastConfiguration safe.Safe
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointIngress holds the endpoint information for the Kubernetes provider.
|
// EndpointIngress holds the endpoint information for the Kubernetes provider.
|
||||||
|
@ -107,6 +108,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.AllowExternalNameServices {
|
||||||
|
logger.Warn("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
|
||||||
|
}
|
||||||
|
|
||||||
pool.GoCtx(func(ctxPool context.Context) {
|
pool.GoCtx(func(ctxPool context.Context) {
|
||||||
operation := func() error {
|
operation := func() error {
|
||||||
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
|
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
|
||||||
|
@ -232,7 +237,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
service, err := loadService(client, ingress.Namespace, *ingress.Spec.DefaultBackend)
|
service, err := p.loadService(client, ingress.Namespace, *ingress.Spec.DefaultBackend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).
|
log.FromContext(ctx).
|
||||||
WithField("serviceName", ingress.Spec.DefaultBackend.Service.Name).
|
WithField("serviceName", ingress.Spec.DefaultBackend.Service.Name).
|
||||||
|
@ -277,7 +282,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pa := range rule.HTTP.Paths {
|
for _, pa := range rule.HTTP.Paths {
|
||||||
service, err := loadService(client, ingress.Namespace, pa.Backend)
|
service, err := p.loadService(client, ingress.Namespace, pa.Backend)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).
|
log.FromContext(ctx).
|
||||||
WithField("serviceName", pa.Backend.Service.Name).
|
WithField("serviceName", pa.Backend.Service.Name).
|
||||||
|
@ -486,7 +491,7 @@ func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores
|
||||||
return configs
|
return configs
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadService(client Client, namespace string, backend networkingv1.IngressBackend) (*dynamic.Service, error) {
|
func (p *Provider) loadService(client Client, namespace string, backend networkingv1.IngressBackend) (*dynamic.Service, error) {
|
||||||
service, exists, err := client.GetService(namespace, backend.Service.Name)
|
service, exists, err := client.GetService(namespace, backend.Service.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -496,6 +501,10 @@ func loadService(client Client, namespace string, backend networkingv1.IngressBa
|
||||||
return nil, errors.New("service not found")
|
return nil, errors.New("service not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !p.AllowExternalNameServices && service.Spec.Type == corev1.ServiceTypeExternalName {
|
||||||
|
return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, backend.Service.Name)
|
||||||
|
}
|
||||||
|
|
||||||
var portName string
|
var portName string
|
||||||
var portSpec corev1.ServicePort
|
var portSpec corev1.ServicePort
|
||||||
var match bool
|
var match bool
|
||||||
|
|
|
@ -746,33 +746,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
desc: "Ingress with service with externalName",
|
|
||||||
expected: &dynamic.Configuration{
|
|
||||||
TCP: &dynamic.TCPConfiguration{},
|
|
||||||
HTTP: &dynamic.HTTPConfiguration{
|
|
||||||
Middlewares: map[string]*dynamic.Middleware{},
|
|
||||||
Routers: map[string]*dynamic.Router{
|
|
||||||
"testing-traefik-tchouk-bar": {
|
|
||||||
Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)",
|
|
||||||
Service: "testing-service1-8080",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Services: map[string]*dynamic.Service{
|
|
||||||
"testing-service1-8080": {
|
|
||||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
|
||||||
PassHostHeader: Bool(true),
|
|
||||||
Servers: []dynamic.Server{
|
|
||||||
{
|
|
||||||
URL: "http://traefik.wtf:8080",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
desc: "Ingress with port invalid for one service",
|
desc: "Ingress with port invalid for one service",
|
||||||
expected: &dynamic.Configuration{
|
expected: &dynamic.Configuration{
|
||||||
|
@ -800,47 +773,6 @@ 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",
|
desc: "TLS support",
|
||||||
expected: &dynamic.Configuration{
|
expected: &dynamic.Configuration{
|
||||||
|
@ -1702,7 +1634,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
clientMock := newClientMock(serverVersion, paths...)
|
clientMock := newClientMock(serverVersion, paths...)
|
||||||
|
|
||||||
p := Provider{IngressClass: test.ingressClass, AllowEmptyServices: test.allowEmptyServices}
|
p := Provider{IngressClass: test.ingressClass, AllowEmptyServices: test.allowEmptyServices}
|
||||||
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
|
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
|
||||||
|
|
||||||
|
@ -1711,6 +1642,154 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
ingressClass string
|
||||||
|
serverVersion string
|
||||||
|
allowExternalNameServices bool
|
||||||
|
expected *dynamic.Configuration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Ingress with service with externalName",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{},
|
||||||
|
Services: map[string]*dynamic.Service{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Ingress with service with externalName enabled",
|
||||||
|
allowExternalNameServices: true,
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-traefik-tchouk-bar": {
|
||||||
|
Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)",
|
||||||
|
Service: "testing-service1-8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-8080": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://traefik.wtf:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Ingress with IPv6 endpoints externalname enabled",
|
||||||
|
allowExternalNameServices: true,
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"example-com-testing-foo": {
|
||||||
|
Rule: "PathPrefix(`/foo`)",
|
||||||
|
Service: "testing-service-foo-8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service-foo-8080": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b]:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var paths []string
|
||||||
|
_, err := os.Stat(generateTestFilename("_ingress", test.desc))
|
||||||
|
if err == nil {
|
||||||
|
paths = append(paths, generateTestFilename("_ingress", test.desc))
|
||||||
|
}
|
||||||
|
_, err = os.Stat(generateTestFilename("_endpoint", test.desc))
|
||||||
|
if err == nil {
|
||||||
|
paths = append(paths, generateTestFilename("_endpoint", test.desc))
|
||||||
|
}
|
||||||
|
_, err = os.Stat(generateTestFilename("_service", test.desc))
|
||||||
|
if err == nil {
|
||||||
|
paths = append(paths, generateTestFilename("_service", test.desc))
|
||||||
|
}
|
||||||
|
_, err = os.Stat(generateTestFilename("_secret", test.desc))
|
||||||
|
if err == nil {
|
||||||
|
paths = append(paths, generateTestFilename("_secret", test.desc))
|
||||||
|
}
|
||||||
|
_, err = os.Stat(generateTestFilename("_ingressclass", test.desc))
|
||||||
|
if err == nil {
|
||||||
|
paths = append(paths, generateTestFilename("_ingressclass", test.desc))
|
||||||
|
}
|
||||||
|
|
||||||
|
serverVersion := test.serverVersion
|
||||||
|
if serverVersion == "" {
|
||||||
|
serverVersion = "v1.17"
|
||||||
|
}
|
||||||
|
|
||||||
|
clientMock := newClientMock(serverVersion, paths...)
|
||||||
|
|
||||||
|
p := Provider{IngressClass: test.ingressClass}
|
||||||
|
p.AllowExternalNameServices = test.allowExternalNameServices
|
||||||
|
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, conf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func generateTestFilename(suffix, desc string) string {
|
func generateTestFilename(suffix, desc string) string {
|
||||||
return "./fixtures/" + strings.ReplaceAll(desc, " ", "-") + suffix + ".yml"
|
return "./fixtures/" + strings.ReplaceAll(desc, " ", "-") + suffix + ".yml"
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,11 @@ func host(route *mux.Route, hosts ...string) error {
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||||
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
||||||
if len(reqHost) == 0 {
|
if len(reqHost) == 0 {
|
||||||
log.FromContext(req.Context()).Warnf("Could not retrieve CanonizedHost, rejecting %s", req.Host)
|
// If the request is an HTTP/1.0 request, then a Host may not be defined.
|
||||||
|
if req.ProtoAtLeast(1, 1) {
|
||||||
|
log.FromContext(req.Context()).Warnf("Could not retrieve CanonizedHost, rejecting %s", req.Host)
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/traefik/traefik/v2/pkg/log"
|
"github.com/traefik/traefik/v2/pkg/log"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider"
|
"github.com/traefik/traefik/v2/pkg/provider"
|
||||||
"github.com/traefik/traefik/v2/pkg/safe"
|
"github.com/traefik/traefik/v2/pkg/safe"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigurationWatcher watches configuration changes.
|
// ConfigurationWatcher watches configuration changes.
|
||||||
|
@ -164,6 +165,16 @@ func (c *ConfigurationWatcher) preLoadConfiguration(configMsg dynamic.Message) {
|
||||||
if copyConf.TLS != nil {
|
if copyConf.TLS != nil {
|
||||||
copyConf.TLS.Certificates = nil
|
copyConf.TLS.Certificates = nil
|
||||||
|
|
||||||
|
if copyConf.TLS.Options != nil {
|
||||||
|
cleanedOptions := make(map[string]tls.Options, len(copyConf.TLS.Options))
|
||||||
|
for name, option := range copyConf.TLS.Options {
|
||||||
|
option.ClientAuth.CAFiles = []tls.FileOrContent{}
|
||||||
|
cleanedOptions[name] = option
|
||||||
|
}
|
||||||
|
|
||||||
|
copyConf.TLS.Options = cleanedOptions
|
||||||
|
}
|
||||||
|
|
||||||
for k := range copyConf.TLS.Stores {
|
for k := range copyConf.TLS.Stores {
|
||||||
st := copyConf.TLS.Stores[k]
|
st := copyConf.TLS.Stores[k]
|
||||||
st.DefaultCertificate = nil
|
st.DefaultCertificate = nil
|
||||||
|
@ -171,6 +182,13 @@ func (c *ConfigurationWatcher) preLoadConfiguration(configMsg dynamic.Message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if copyConf.HTTP != nil {
|
||||||
|
for _, transport := range copyConf.HTTP.ServersTransports {
|
||||||
|
transport.Certificates = tls.Certificates{}
|
||||||
|
transport.RootCAs = []tls.FileOrContent{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jsonConf, err := json.Marshal(copyConf)
|
jsonConf, err := json.Marshal(copyConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Could not marshal dynamic configuration: %v", err)
|
logger.Errorf("Could not marshal dynamic configuration: %v", err)
|
||||||
|
|
Loading…
Reference in a new issue