Merge current v2.4 into v2.5

This commit is contained in:
romain 2021-07-13 18:12:29 +02:00
commit 3a180e2afc
28 changed files with 847 additions and 166 deletions

View file

@ -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

View file

@ -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.

View 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

View file

@ -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

View file

@ -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,

View file

@ -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).

View file

@ -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).

View file

@ -17,3 +17,4 @@
[providers.kubernetesCRD] [providers.kubernetesCRD]
allowCrossNamespace = false allowCrossNamespace = false
allowExternalNameServices = true

View file

@ -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"
} }

View file

@ -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, ","))
} }
}) })
} }

View file

@ -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()
}

View file

@ -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

View file

@ -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) {

View file

@ -0,0 +1 @@
CONTENTKEY

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: external.service.with.port
port: 80

View file

@ -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
} }

View file

@ -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

View file

@ -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

View file

@ -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)
}) })

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
} }

View file

@ -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
} }

View file

@ -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)