Add Traefik Hub Integration (Experimental Feature)
This commit is contained in:
parent
8d58f33a28
commit
fbdb6e6e78
20 changed files with 992 additions and 11 deletions
|
@ -241,6 +241,19 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Traefik Hub
|
||||||
|
|
||||||
|
if staticConfiguration.Hub != nil {
|
||||||
|
if err = providerAggregator.AddProvider(staticConfiguration.Hub); err != nil {
|
||||||
|
return nil, fmt.Errorf("adding Traefik Hub provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// API is mandatory for Traefik Hub to access the dynamic configuration.
|
||||||
|
if staticConfiguration.API == nil {
|
||||||
|
staticConfiguration.API = &static.API{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Metrics
|
// Metrics
|
||||||
|
|
||||||
metricRegistries := registerMetricClients(staticConfiguration.Metrics)
|
metricRegistries := registerMetricClients(staticConfiguration.Metrics)
|
||||||
|
@ -323,7 +336,10 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok {
|
if _, ok := resolverNames[rt.TLS.CertResolver]; !ok &&
|
||||||
|
// "traefik-hub" is an allowed certificate resolver name in a Traefik Hub Experimental feature context.
|
||||||
|
// It is used to activate its own certificate resolution, even though it is not a "classical" traefik certificate resolver.
|
||||||
|
(staticConfiguration.Hub == nil || rt.TLS.CertResolver != "traefik-hub") {
|
||||||
log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver)
|
log.WithoutContext().Errorf("the router %s uses a non-existent resolver: %s", rtName, rt.TLS.CertResolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -346,6 +362,11 @@ func getHTTPChallengeHandler(acmeProviders []*acme.Provider, httpChallengeProvid
|
||||||
func getDefaultsEntrypoints(staticConfiguration *static.Configuration) []string {
|
func getDefaultsEntrypoints(staticConfiguration *static.Configuration) []string {
|
||||||
var defaultEntryPoints []string
|
var defaultEntryPoints []string
|
||||||
for name, cfg := range staticConfiguration.EntryPoints {
|
for name, cfg := range staticConfiguration.EntryPoints {
|
||||||
|
// Traefik Hub entryPoint should not be part of the set of default entryPoints.
|
||||||
|
if staticConfiguration.Hub != nil && staticConfiguration.Hub.EntryPoint == name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
protocol, err := cfg.GetProtocol()
|
protocol, err := cfg.GetProtocol()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Should never happen because Traefik should not start if protocol is invalid.
|
// Should never happen because Traefik should not start if protocol is invalid.
|
||||||
|
|
|
@ -183,6 +183,9 @@ Timeout defines how long to wait on an idle session before releasing the related
|
||||||
`--experimental.http3`:
|
`--experimental.http3`:
|
||||||
Enable HTTP3. (Default: ```false```)
|
Enable HTTP3. (Default: ```false```)
|
||||||
|
|
||||||
|
`--experimental.hub`:
|
||||||
|
Enable the Traefik Hub provider. (Default: ```false```)
|
||||||
|
|
||||||
`--experimental.kubernetesgateway`:
|
`--experimental.kubernetesgateway`:
|
||||||
Allow the Kubernetes gateway api provider usage. (Default: ```false```)
|
Allow the Kubernetes gateway api provider usage. (Default: ```false```)
|
||||||
|
|
||||||
|
@ -216,6 +219,24 @@ resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```)
|
||||||
`--hostresolver.resolvdepth`:
|
`--hostresolver.resolvdepth`:
|
||||||
The maximal depth of DNS recursive resolving (Default: ```5```)
|
The maximal depth of DNS recursive resolving (Default: ```5```)
|
||||||
|
|
||||||
|
`--hub`:
|
||||||
|
Traefik Hub configuration. (Default: ```false```)
|
||||||
|
|
||||||
|
`--hub.entrypoint`:
|
||||||
|
Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router. (Default: ```traefik-hub```)
|
||||||
|
|
||||||
|
`--hub.tls.ca`:
|
||||||
|
The certificate authority authenticates the Traefik Hub Agent certificate.
|
||||||
|
|
||||||
|
`--hub.tls.cert`:
|
||||||
|
The TLS certificate for Traefik Proxy as a TLS client.
|
||||||
|
|
||||||
|
`--hub.tls.insecure`:
|
||||||
|
Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. (Default: ```false```)
|
||||||
|
|
||||||
|
`--hub.tls.key`:
|
||||||
|
The TLS key for Traefik Proxy as a TLS client.
|
||||||
|
|
||||||
`--log`:
|
`--log`:
|
||||||
Traefik log settings. (Default: ```false```)
|
Traefik log settings. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
@ -183,6 +183,9 @@ Timeout defines how long to wait on an idle session before releasing the related
|
||||||
`TRAEFIK_EXPERIMENTAL_HTTP3`:
|
`TRAEFIK_EXPERIMENTAL_HTTP3`:
|
||||||
Enable HTTP3. (Default: ```false```)
|
Enable HTTP3. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_EXPERIMENTAL_HUB`:
|
||||||
|
Enable the Traefik Hub provider. (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_EXPERIMENTAL_KUBERNETESGATEWAY`:
|
`TRAEFIK_EXPERIMENTAL_KUBERNETESGATEWAY`:
|
||||||
Allow the Kubernetes gateway api provider usage. (Default: ```false```)
|
Allow the Kubernetes gateway api provider usage. (Default: ```false```)
|
||||||
|
|
||||||
|
@ -216,6 +219,24 @@ resolv.conf used for DNS resolving (Default: ```/etc/resolv.conf```)
|
||||||
`TRAEFIK_HOSTRESOLVER_RESOLVDEPTH`:
|
`TRAEFIK_HOSTRESOLVER_RESOLVDEPTH`:
|
||||||
The maximal depth of DNS recursive resolving (Default: ```5```)
|
The maximal depth of DNS recursive resolving (Default: ```5```)
|
||||||
|
|
||||||
|
`TRAEFIK_HUB`:
|
||||||
|
Traefik Hub configuration. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_HUB_ENTRYPOINT`:
|
||||||
|
Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router. (Default: ```traefik-hub```)
|
||||||
|
|
||||||
|
`TRAEFIK_HUB_TLS_CA`:
|
||||||
|
The certificate authority authenticates the Traefik Hub Agent certificate.
|
||||||
|
|
||||||
|
`TRAEFIK_HUB_TLS_CERT`:
|
||||||
|
The TLS certificate for Traefik Proxy as a TLS client.
|
||||||
|
|
||||||
|
`TRAEFIK_HUB_TLS_INSECURE`:
|
||||||
|
Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_HUB_TLS_KEY`:
|
||||||
|
The TLS key for Traefik Proxy as a TLS client.
|
||||||
|
|
||||||
`TRAEFIK_LOG`:
|
`TRAEFIK_LOG`:
|
||||||
Traefik log settings. (Default: ```false```)
|
Traefik log settings. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
@ -419,9 +419,18 @@
|
||||||
token = "foobar"
|
token = "foobar"
|
||||||
dashboard = true
|
dashboard = true
|
||||||
|
|
||||||
|
[hub]
|
||||||
|
entrypoint = "foobar"
|
||||||
|
[hub.tls]
|
||||||
|
insecure = true
|
||||||
|
ca = "foobar"
|
||||||
|
cert = "foobar"
|
||||||
|
key = "foobar"
|
||||||
|
|
||||||
[experimental]
|
[experimental]
|
||||||
kubernetesGateway = true
|
kubernetesGateway = true
|
||||||
http3 = true
|
http3 = true
|
||||||
|
hub = true
|
||||||
[experimental.plugins]
|
[experimental.plugins]
|
||||||
[experimental.plugins.Descriptor0]
|
[experimental.plugins.Descriptor0]
|
||||||
moduleName = "foobar"
|
moduleName = "foobar"
|
||||||
|
|
|
@ -440,9 +440,17 @@ certificatesResolvers:
|
||||||
pilot:
|
pilot:
|
||||||
token: foobar
|
token: foobar
|
||||||
dashboard: true
|
dashboard: true
|
||||||
|
hub:
|
||||||
|
entrypoint: foobar
|
||||||
|
tls:
|
||||||
|
insecure: true
|
||||||
|
ca: foobar
|
||||||
|
cert: foobar
|
||||||
|
key: foobar
|
||||||
experimental:
|
experimental:
|
||||||
kubernetesGateway: true
|
kubernetesGateway: true
|
||||||
http3: true
|
http3: true
|
||||||
|
hub: true
|
||||||
plugins:
|
plugins:
|
||||||
Descriptor0:
|
Descriptor0:
|
||||||
moduleName: foobar
|
moduleName: foobar
|
||||||
|
|
294
docs/content/traefik-hub/index.md
Normal file
294
docs/content/traefik-hub/index.md
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
# Traefik Hub (Experimental)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Once the Traefik Hub Experimental feature is enabled in Traefik,
|
||||||
|
Traefik and its local agent communicate together.
|
||||||
|
This agent can:
|
||||||
|
|
||||||
|
* get the Traefik metrics to display them in the Traefik Hub UI
|
||||||
|
* secure the Traefik routers
|
||||||
|
* provide ACME certificates to Traefik
|
||||||
|
* transfer requests from the SaaS Platform to Traefik (and then avoid the users to expose directly their infrastructure on the internet)
|
||||||
|
|
||||||
|
!!! warning "Traefik Hub EntryPoint"
|
||||||
|
|
||||||
|
When the Traefik Hub feature is enabled, Traefik exposes some services meant for the Traefik Hub Agent on a dedicated entryPoint (on port `9900` by default).
|
||||||
|
Given their sensitive nature, those services should not be publicly exposed.
|
||||||
|
Also this dedicated entryPoint, regardless of how it is created (default, or user-defined), should not be used by anything other than the Hub Agent.
|
||||||
|
|
||||||
|
!!! important "Learn More About Traefik Hub"
|
||||||
|
|
||||||
|
This section is intended only as a brief overview for Traefik users who are not familiar with Traefik Hub.
|
||||||
|
To explore all that Traefik Hub has to offer, please consult the [Traefik Hub Documentation](https://doc.traefik.io/traefik-hub).
|
||||||
|
|
||||||
|
!!! Note "Prerequisites"
|
||||||
|
|
||||||
|
* Traefik Hub is compatible with Traefik Proxy 2.7 or later.
|
||||||
|
* The Traefik Hub Agent must be installed to connect to the Traefik Hub platform.
|
||||||
|
* Activate this feature in the experimental section of the static configuration.
|
||||||
|
|
||||||
|
!!! example "Minimal Static Configuration to Activate Traefik Hub"
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
experimental:
|
||||||
|
hub: true
|
||||||
|
|
||||||
|
hub:
|
||||||
|
tls:
|
||||||
|
insecure: true
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
prometheus: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[experimental]
|
||||||
|
hub = true
|
||||||
|
|
||||||
|
[hub]
|
||||||
|
[hub.tls]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[metrics]
|
||||||
|
[metrics.prometheus]
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--experimental.hub
|
||||||
|
--hub.tls.insecure=true
|
||||||
|
--metrics.prometheus=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### `entryPoint`
|
||||||
|
|
||||||
|
_Optional, Default="traefik-hub"_
|
||||||
|
|
||||||
|
Defines the entryPoint that exposes data for Traefik Hub Agent.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
* If no entryPoint is defined, a default `traefik-hub` entryPoint is created (on port `9900`).
|
||||||
|
* If defined, the value must match an existing entryPoint name.
|
||||||
|
* This dedicated Traefik Hub entryPoint should not be used by anything other than Traefik Hub.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
entryPoints:
|
||||||
|
hub-ep: ":8000"
|
||||||
|
|
||||||
|
hub:
|
||||||
|
entryPoint: "hub-ep"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[entryPoints.hub-ep]
|
||||||
|
address = ":8000"
|
||||||
|
|
||||||
|
[hub]
|
||||||
|
entryPoint = "hub-ep"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--entrypoints.hub-ep.address=:8000
|
||||||
|
--hub.entrypoint=hub-ep
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tls`
|
||||||
|
|
||||||
|
_Required, Default=None_
|
||||||
|
|
||||||
|
This section allows configuring mutual TLS connection between Traefik Proxy and the Traefik Hub Agent.
|
||||||
|
The key and the certificate are the credentials for Traefik Proxy as a TLS client.
|
||||||
|
The certificate authority authenticates the Traefik Hub Agent certificate.
|
||||||
|
|
||||||
|
!!! note "Certificate Domain"
|
||||||
|
|
||||||
|
The certificate must be valid for the `proxy.traefik` domain.
|
||||||
|
|
||||||
|
!!! note "Certificates Definition"
|
||||||
|
|
||||||
|
Certificates can be defined either by their content or their path.
|
||||||
|
|
||||||
|
!!! note "Insecure Mode"
|
||||||
|
|
||||||
|
The `insecure` option is mutually exclusive with any other option.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
hub:
|
||||||
|
tls:
|
||||||
|
ca: /path/to/ca.pem
|
||||||
|
cert: /path/to/cert.pem
|
||||||
|
key: /path/to/key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[hub.tls]
|
||||||
|
ca= "/path/to/ca.pem"
|
||||||
|
cert= "/path/to/cert.pem"
|
||||||
|
key= "/path/to/key.pem"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--hub.tls.ca=/path/to/ca.pem
|
||||||
|
--hub.tls.cert=/path/to/cert.pem
|
||||||
|
--hub.tls.key=/path/to/key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tls.ca`
|
||||||
|
|
||||||
|
The certificate authority authenticates the Traefik Hub Agent certificate.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
hub:
|
||||||
|
tls:
|
||||||
|
ca: |-
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
|
||||||
|
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
|
||||||
|
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||||
|
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
|
||||||
|
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||||
|
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
|
||||||
|
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
|
||||||
|
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[hub.tls]
|
||||||
|
ca = """-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
|
||||||
|
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
|
||||||
|
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||||
|
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
|
||||||
|
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||||
|
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
|
||||||
|
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
|
||||||
|
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
|
||||||
|
-----END CERTIFICATE-----"""
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--hub.tls.ca=-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
|
||||||
|
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
|
||||||
|
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||||
|
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
|
||||||
|
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||||
|
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
|
||||||
|
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
|
||||||
|
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tls.cert`
|
||||||
|
|
||||||
|
The TLS certificate for Traefik Proxy as a TLS client.
|
||||||
|
|
||||||
|
!!! note "Certificate Domain"
|
||||||
|
|
||||||
|
The certificate must be valid for the `proxy.traefik` domain.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
hub:
|
||||||
|
tls:
|
||||||
|
ca: |-
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
|
||||||
|
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
|
||||||
|
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||||
|
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
|
||||||
|
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||||
|
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
|
||||||
|
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
|
||||||
|
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[hub.tls]
|
||||||
|
cert = """-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
|
||||||
|
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
|
||||||
|
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||||
|
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
|
||||||
|
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||||
|
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
|
||||||
|
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
|
||||||
|
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
|
||||||
|
-----END CERTIFICATE-----"""
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--hub.tls.cert=-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBcjCCARegAwIBAgIQaewCzGdRz5iNnjAiEoO5AzAKBggqhkjOPQQDAjASMRAw
|
||||||
|
DgYDVQQKEwdBY21lIENvMCAXDTIyMDMyMTE2MTY0NFoYDzIxMjIwMjI1MTYxNjQ0
|
||||||
|
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||||
|
ZaKYPj2G8Hnmju6jbHt+vODwKqNDVQMH5nxhtAgSUZS61mLWwZvvUhIYLNPwHz8a
|
||||||
|
x8C7+cuihEC6Tzvn8DeGeKNNMEswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||||
|
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFgYDVR0RBA8wDYILZXhhbXBsZS5jb20w
|
||||||
|
CgYIKoZIzj0EAwIDSQAwRgIhAO8sucDGY+JOrNgQg1a9ZqqYvbxPFnYsSZr7F/vz
|
||||||
|
aUX2AiEAilZ+M5eX4RiMFc3nlm9qVs1LZhV3dZW/u80/mPQ/oaY=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tls.key`
|
||||||
|
|
||||||
|
The TLS key for Traefik Proxy as a TLS client.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
hub:
|
||||||
|
tls:
|
||||||
|
key: |-
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
|
||||||
|
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
|
||||||
|
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[hub.tls]
|
||||||
|
key = """-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
|
||||||
|
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
|
||||||
|
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
|
||||||
|
-----END PRIVATE KEY-----"""
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--hub.tls.key=-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgm+XJ3LVrTbbirJea
|
||||||
|
O+Crj2ADVsVHjMuiyd72VE3lgxihRANCAARlopg+PYbweeaO7qNse3684PAqo0NV
|
||||||
|
AwfmfGG0CBJRlLrWYtbBm+9SEhgs0/AfPxrHwLv5y6KEQLpPO+fwN4Z4
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tls.insecure`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
Enables an insecure TLS connection that uses default credentials,
|
||||||
|
and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent.
|
||||||
|
The `insecure` option is mutually exclusive with any other option.
|
||||||
|
|
||||||
|
!!! warning "Security Consideration"
|
||||||
|
|
||||||
|
Do not use this setup in production.
|
||||||
|
This option implies sensitive data can be exposed to potential malicious third-party programs.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
hub:
|
||||||
|
insecure: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[hub]
|
||||||
|
insecure = true
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--hub.insecure=true
|
||||||
|
```
|
|
@ -135,6 +135,7 @@ nav:
|
||||||
- 'InFlightConn': 'middlewares/tcp/inflightconn.md'
|
- 'InFlightConn': 'middlewares/tcp/inflightconn.md'
|
||||||
- 'IpWhitelist': 'middlewares/tcp/ipwhitelist.md'
|
- 'IpWhitelist': 'middlewares/tcp/ipwhitelist.md'
|
||||||
- 'Plugins & Traefik Pilot': 'plugins/index.md'
|
- 'Plugins & Traefik Pilot': 'plugins/index.md'
|
||||||
|
- 'Traefik Hub': 'traefik-hub/index.md'
|
||||||
- 'Operations':
|
- 'Operations':
|
||||||
- 'CLI': 'operations/cli.md'
|
- 'CLI': 'operations/cli.md'
|
||||||
- 'Dashboard' : 'operations/dashboard.md'
|
- 'Dashboard' : 'operations/dashboard.md'
|
||||||
|
|
|
@ -4,3 +4,4 @@ mkdocs-bootswatch==1.0
|
||||||
mkdocs-traefiklabs>=100.0.7
|
mkdocs-traefiklabs>=100.0.7
|
||||||
markdown-include==0.5.1
|
markdown-include==0.5.1
|
||||||
mkdocs-exclude==1.0.2
|
mkdocs-exclude==1.0.2
|
||||||
|
Jinja2==3.0.0
|
||||||
|
|
|
@ -26,6 +26,7 @@ type features struct {
|
||||||
Tracing string `json:"tracing"`
|
Tracing string `json:"tracing"`
|
||||||
Metrics string `json:"metrics"`
|
Metrics string `json:"metrics"`
|
||||||
AccessLog bool `json:"accessLog"`
|
AccessLog bool `json:"accessLog"`
|
||||||
|
Hub bool `json:"hub"`
|
||||||
// TODO add certificates resolvers
|
// TODO add certificates resolvers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,6 +248,7 @@ func getFeatures(conf static.Configuration) features {
|
||||||
Tracing: getTracing(conf),
|
Tracing: getTracing(conf),
|
||||||
Metrics: getMetrics(conf),
|
Metrics: getMetrics(conf),
|
||||||
AccessLog: conf.AccessLog != nil,
|
AccessLog: conf.AccessLog != nil,
|
||||||
|
Hub: conf.Hub != nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/traefik/traefik/v2/pkg/config/static"
|
"github.com/traefik/traefik/v2/pkg/config/static"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/docker"
|
"github.com/traefik/traefik/v2/pkg/provider/docker"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/file"
|
"github.com/traefik/traefik/v2/pkg/provider/file"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/provider/hub"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd"
|
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress"
|
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/marathon"
|
"github.com/traefik/traefik/v2/pkg/provider/marathon"
|
||||||
|
@ -265,6 +266,7 @@ func TestHandler_Overview(t *testing.T) {
|
||||||
Tracing: &static.Tracing{
|
Tracing: &static.Tracing{
|
||||||
Jaeger: &jaeger.Config{},
|
Jaeger: &jaeger.Config{},
|
||||||
},
|
},
|
||||||
|
Hub: &hub.Provider{},
|
||||||
},
|
},
|
||||||
confDyn: runtime.Configuration{},
|
confDyn: runtime.Configuration{},
|
||||||
expected: expected{
|
expected: expected{
|
||||||
|
|
5
pkg/api/testdata/overview-dynamic.json
vendored
5
pkg/api/testdata/overview-dynamic.json
vendored
|
@ -2,7 +2,8 @@
|
||||||
"features": {
|
"features": {
|
||||||
"accessLog": false,
|
"accessLog": false,
|
||||||
"metrics": "",
|
"metrics": "",
|
||||||
"tracing": ""
|
"tracing": "",
|
||||||
|
"hub": false
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"middlewares": {
|
"middlewares": {
|
||||||
|
@ -50,4 +51,4 @@
|
||||||
"warnings": 0
|
"warnings": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
pkg/api/testdata/overview-empty.json
vendored
5
pkg/api/testdata/overview-empty.json
vendored
|
@ -2,7 +2,8 @@
|
||||||
"features": {
|
"features": {
|
||||||
"accessLog": false,
|
"accessLog": false,
|
||||||
"metrics": "",
|
"metrics": "",
|
||||||
"tracing": ""
|
"tracing": "",
|
||||||
|
"hub": false
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"middlewares": {
|
"middlewares": {
|
||||||
|
@ -50,4 +51,4 @@
|
||||||
"warnings": 0
|
"warnings": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
pkg/api/testdata/overview-features.json
vendored
5
pkg/api/testdata/overview-features.json
vendored
|
@ -2,7 +2,8 @@
|
||||||
"features": {
|
"features": {
|
||||||
"accessLog": false,
|
"accessLog": false,
|
||||||
"metrics": "Prometheus",
|
"metrics": "Prometheus",
|
||||||
"tracing": "Jaeger"
|
"tracing": "Jaeger",
|
||||||
|
"hub": true
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"middlewares": {
|
"middlewares": {
|
||||||
|
@ -50,4 +51,4 @@
|
||||||
"warnings": 0
|
"warnings": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
pkg/api/testdata/overview-providers.json
vendored
5
pkg/api/testdata/overview-providers.json
vendored
|
@ -2,7 +2,8 @@
|
||||||
"features": {
|
"features": {
|
||||||
"accessLog": false,
|
"accessLog": false,
|
||||||
"metrics": "",
|
"metrics": "",
|
||||||
"tracing": ""
|
"tracing": "",
|
||||||
|
"hub": false
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"middlewares": {
|
"middlewares": {
|
||||||
|
@ -60,4 +61,4 @@
|
||||||
"warnings": 0
|
"warnings": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,4 +9,5 @@ type Experimental struct {
|
||||||
|
|
||||||
KubernetesGateway bool `description:"Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"`
|
KubernetesGateway bool `description:"Allow the Kubernetes gateway api provider usage." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" export:"true"`
|
||||||
HTTP3 bool `description:"Enable HTTP3." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" export:"true"`
|
HTTP3 bool `description:"Enable HTTP3." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" export:"true"`
|
||||||
|
Hub bool `description:"Enable the Traefik Hub provider." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" export:"true"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package static
|
package static
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
stdlog "log"
|
stdlog "log"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -17,6 +18,7 @@ import (
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/ecs"
|
"github.com/traefik/traefik/v2/pkg/provider/ecs"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/file"
|
"github.com/traefik/traefik/v2/pkg/provider/file"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/http"
|
"github.com/traefik/traefik/v2/pkg/provider/http"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/provider/hub"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd"
|
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/gateway"
|
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/gateway"
|
||||||
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress"
|
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/ingress"
|
||||||
|
@ -79,6 +81,8 @@ type Configuration struct {
|
||||||
// Deprecated.
|
// Deprecated.
|
||||||
Pilot *Pilot `description:"Traefik Pilot configuration." json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" export:"true"`
|
Pilot *Pilot `description:"Traefik Pilot configuration." json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" export:"true"`
|
||||||
|
|
||||||
|
Hub *hub.Provider `description:"Traefik Hub configuration." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||||
|
|
||||||
Experimental *Experimental `description:"experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"`
|
Experimental *Experimental `description:"experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,10 +201,14 @@ type Providers struct {
|
||||||
// It also takes care of maintaining backwards compatibility.
|
// It also takes care of maintaining backwards compatibility.
|
||||||
func (c *Configuration) SetEffectiveConfiguration() {
|
func (c *Configuration) SetEffectiveConfiguration() {
|
||||||
// Creates the default entry point if needed
|
// Creates the default entry point if needed
|
||||||
if len(c.EntryPoints) == 0 {
|
if len(c.EntryPoints) == 0 || (c.Hub != nil && len(c.EntryPoints) == 1 && c.EntryPoints[c.Hub.EntryPoint] != nil) {
|
||||||
ep := &EntryPoint{Address: ":80"}
|
ep := &EntryPoint{Address: ":80"}
|
||||||
ep.SetDefaults()
|
ep.SetDefaults()
|
||||||
c.EntryPoints = EntryPoints{"http": ep}
|
// TODO: double check this tomorrow
|
||||||
|
if c.EntryPoints == nil {
|
||||||
|
c.EntryPoints = make(EntryPoints)
|
||||||
|
}
|
||||||
|
c.EntryPoints["http"] = ep
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates the internal traefik entry point if needed
|
// Creates the internal traefik entry point if needed
|
||||||
|
@ -215,6 +223,15 @@ func (c *Configuration) SetEffectiveConfiguration() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Hub != nil {
|
||||||
|
if err := c.initHubProvider(); err != nil {
|
||||||
|
c.Hub = nil
|
||||||
|
log.WithoutContext().Errorf("Unable to activate the Hub provider: %v", err)
|
||||||
|
} else {
|
||||||
|
log.WithoutContext().Debugf("Experimental Hub provider has been activated.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if c.Providers.Docker != nil {
|
if c.Providers.Docker != nil {
|
||||||
if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 {
|
if c.Providers.Docker.SwarmModeRefreshSeconds <= 0 {
|
||||||
c.Providers.Docker.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second)
|
c.Providers.Docker.SwarmModeRefreshSeconds = ptypes.Duration(15 * time.Second)
|
||||||
|
@ -280,6 +297,46 @@ func (c *Configuration) initACMEProvider() {
|
||||||
legolog.Logger = stdlog.New(log.WithoutContext().WriterLevel(logrus.DebugLevel), "legolog: ", 0)
|
legolog.Logger = stdlog.New(log.WithoutContext().WriterLevel(logrus.DebugLevel), "legolog: ", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) initHubProvider() error {
|
||||||
|
// Hub provider is an experimental feature. Require the experimental flag to be enabled before continuing.
|
||||||
|
if c.Experimental == nil || !c.Experimental.Hub {
|
||||||
|
return errors.New("experimental flag for Hub not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Hub.TLS == nil {
|
||||||
|
return errors.New("no TLS configuration defined for Hub")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Hub.TLS.Insecure && (c.Hub.TLS.CA != "" || c.Hub.TLS.Cert != "" || c.Hub.TLS.Key != "") {
|
||||||
|
return errors.New("mTLS configuration for Hub and insecure TLS for Hub are mutually exclusive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.Hub.TLS.Insecure && (c.Hub.TLS.CA == "" || c.Hub.TLS.Cert == "" || c.Hub.TLS.Key == "") {
|
||||||
|
return errors.New("incomplete mTLS configuration for Hub")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Hub.TLS.Insecure {
|
||||||
|
log.WithoutContext().Warn("Hub is in `insecure` mode. Do not run in production with this setup.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates the internal Hub entry point if needed.
|
||||||
|
if c.Hub.EntryPoint == hub.DefaultEntryPointName {
|
||||||
|
if _, ok := c.EntryPoints[hub.DefaultEntryPointName]; !ok {
|
||||||
|
var ep EntryPoint
|
||||||
|
ep.SetDefaults()
|
||||||
|
ep.Address = ":9900"
|
||||||
|
c.EntryPoints[hub.DefaultEntryPointName] = &ep
|
||||||
|
log.WithoutContext().Infof("The entryPoint %q is created on port 9900 to allow Traefik to communicate with the Hub Agent for Traefik.", hub.DefaultEntryPointName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.EntryPoints[c.Hub.EntryPoint].HTTP.TLS = &TLSConfig{
|
||||||
|
Options: "traefik-hub",
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateConfiguration validate that configuration is coherent.
|
// ValidateConfiguration validate that configuration is coherent.
|
||||||
func (c *Configuration) ValidateConfiguration() error {
|
func (c *Configuration) ValidateConfiguration() error {
|
||||||
var acmeEmail string
|
var acmeEmail string
|
||||||
|
|
146
pkg/provider/hub/handler.go
Normal file
146
pkg/provider/hub/handler.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
mux *http.ServeMux
|
||||||
|
|
||||||
|
client http.Client
|
||||||
|
|
||||||
|
entryPoint string
|
||||||
|
port int
|
||||||
|
tlsCfg *TLS
|
||||||
|
|
||||||
|
// Accessed atomically.
|
||||||
|
lastCfgUnixNano int64
|
||||||
|
|
||||||
|
cfgChan chan<- dynamic.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHandler(entryPoint string, port int, cfgChan chan<- dynamic.Message, tlsCfg *TLS, client http.Client) http.Handler {
|
||||||
|
h := &handler{
|
||||||
|
mux: http.NewServeMux(),
|
||||||
|
entryPoint: entryPoint,
|
||||||
|
port: port,
|
||||||
|
cfgChan: cfgChan,
|
||||||
|
tlsCfg: tlsCfg,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mux.HandleFunc("/config", h.handleConfig)
|
||||||
|
h.mux.HandleFunc("/discover-ip", h.handleDiscoverIP)
|
||||||
|
h.mux.HandleFunc("/state", h.handleState)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
type configRequest struct {
|
||||||
|
UnixNano int64 `json:"unixNano"`
|
||||||
|
Configuration *dynamic.Configuration `json:"configuration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) handleConfig(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := &configRequest{Configuration: emptyDynamicConfiguration()}
|
||||||
|
if err := json.NewDecoder(req.Body).Decode(payload); err != nil {
|
||||||
|
err = fmt.Errorf("decoding config request: %w", err)
|
||||||
|
log.WithoutContext().Errorf("Handling config: %v", err)
|
||||||
|
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := payload.Configuration
|
||||||
|
patchDynamicConfiguration(cfg, h.entryPoint, h.port, h.tlsCfg)
|
||||||
|
|
||||||
|
// We can safely drop messages here if the other end is not ready to receive them
|
||||||
|
// as the agent will re-apply the same configuration.
|
||||||
|
select {
|
||||||
|
case h.cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}:
|
||||||
|
atomic.StoreInt64(&h.lastCfgUnixNano, payload.UnixNano)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) handleDiscoverIP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xff := req.Header.Get("X-Forwarded-For")
|
||||||
|
port := req.URL.Query().Get("port")
|
||||||
|
nonce := req.URL.Query().Get("nonce")
|
||||||
|
|
||||||
|
if err := h.doDiscoveryReq(req.Context(), xff, port, nonce); err != nil {
|
||||||
|
err = fmt.Errorf("doing discovery request: %w", err)
|
||||||
|
log.WithoutContext().Errorf("Handling IP discovery: %v", err)
|
||||||
|
http.Error(rw, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(rw).Encode(xff); err != nil {
|
||||||
|
err = fmt.Errorf("encoding discover ip response: %w", err)
|
||||||
|
log.WithoutContext().Errorf("Handling IP discovery: %v", err)
|
||||||
|
http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) doDiscoveryReq(ctx context.Context, ip, port, nonce string) error {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s:%s", ip, port), http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := make(url.Values)
|
||||||
|
q.Set("nonce", nonce)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
req.Host = "agent.traefik"
|
||||||
|
|
||||||
|
resp, err := h.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("doing request: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type stateResponse struct {
|
||||||
|
LastConfigUnixNano int64 `json:"lastConfigUnixNano"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) handleState(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := stateResponse{
|
||||||
|
LastConfigUnixNano: atomic.LoadInt64(&h.lastCfgUnixNano),
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(rw).Encode(resp); err != nil {
|
||||||
|
err = fmt.Errorf("encoding last config received response: %w", err)
|
||||||
|
log.WithoutContext().Errorf("Handling state: %v", err)
|
||||||
|
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
h.mux.ServeHTTP(rw, req)
|
||||||
|
}
|
168
pkg/provider/hub/handler_test.go
Normal file
168
pkg/provider/hub/handler_test.go
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/tls/generate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleConfig(t *testing.T) {
|
||||||
|
cfgChan := make(chan dynamic.Message, 1)
|
||||||
|
|
||||||
|
client, err := createAgentClient(&TLS{Insecure: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
|
||||||
|
|
||||||
|
cfg := emptyDynamicConfiguration()
|
||||||
|
cfg.HTTP.Routers["foo"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{"ep"},
|
||||||
|
Service: "bar",
|
||||||
|
Rule: "Host(`foo.com`)",
|
||||||
|
}
|
||||||
|
|
||||||
|
req := configRequest{Configuration: cfg}
|
||||||
|
|
||||||
|
b, err := json.Marshal(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
server := httptest.NewServer(h)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
resp, err := http.Post(server.URL+"/config", "application/json", bytes.NewReader(b))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case gotCfgRaw := <-cfgChan:
|
||||||
|
patchDynamicConfiguration(cfg, "traefik-hub-ep", 42, nil)
|
||||||
|
assert.Equal(t, cfg, gotCfgRaw.Configuration)
|
||||||
|
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("Configuration not received")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_Config_MethodNotAllowed(t *testing.T) {
|
||||||
|
cfgChan := make(chan dynamic.Message, 1)
|
||||||
|
client, err := createAgentClient(&TLS{Insecure: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
|
||||||
|
|
||||||
|
server := httptest.NewServer(h)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
resp, err := http.Get(server.URL + "/config")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = resp.Body.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_DiscoverIP(t *testing.T) {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
nonce := "XVlBzgbaiCMRAjWw"
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
var handlerCallCount int
|
||||||
|
mux.HandleFunc("/", func(_ http.ResponseWriter, req *http.Request) {
|
||||||
|
handlerCallCount++
|
||||||
|
assert.Equal(t, nonce, req.URL.Query().Get("nonce"))
|
||||||
|
})
|
||||||
|
|
||||||
|
certificate, err := generate.DefaultCertificate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
agentServer := &http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*certificate},
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = agentServer.Close() })
|
||||||
|
|
||||||
|
rdy := make(chan struct{})
|
||||||
|
|
||||||
|
go func(s *http.Server) {
|
||||||
|
close(rdy)
|
||||||
|
if err = s.ServeTLS(listener, "", ""); errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(agentServer)
|
||||||
|
|
||||||
|
<-rdy
|
||||||
|
|
||||||
|
cfgChan := make(chan dynamic.Message, 1)
|
||||||
|
client, err := createAgentClient(&TLS{Insecure: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
|
||||||
|
|
||||||
|
traefikServer := httptest.NewServer(h)
|
||||||
|
t.Cleanup(traefikServer.Close)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, traefikServer.URL+"/discover-ip", http.NoBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
q := make(url.Values)
|
||||||
|
q.Set("port", strconv.Itoa(port))
|
||||||
|
q.Set("nonce", nonce)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
// Simulate a call from behind different proxies.
|
||||||
|
req.Header.Add("X-Forwarded-For", "127.0.0.1")
|
||||||
|
req.Header.Add("X-Forwarded-For", "10.10.0.13")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = resp.Body.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
assert.Equal(t, 1, handlerCallCount)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var ip string
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&ip)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "127.0.0.1", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle_DiscoverIP_MethodNotAllowed(t *testing.T) {
|
||||||
|
cfgChan := make(chan dynamic.Message, 1)
|
||||||
|
client, err := createAgentClient(&TLS{Insecure: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
h := newHandler("traefik-hub-ep", 42, cfgChan, nil, client)
|
||||||
|
|
||||||
|
server := httptest.NewServer(h)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
resp, err := http.Post(server.URL+"/discover-ip", "", http.NoBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = resp.Body.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
|
||||||
|
}
|
215
pkg/provider/hub/hub.go
Normal file
215
pkg/provider/hub/hub.go
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/log"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/provider"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/safe"
|
||||||
|
ttls "github.com/traefik/traefik/v2/pkg/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ provider.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
|
// DefaultEntryPointName is the name of the default internal entry point.
|
||||||
|
const DefaultEntryPointName = "traefik-hub"
|
||||||
|
|
||||||
|
// Provider holds configurations of the provider.
|
||||||
|
type Provider struct {
|
||||||
|
EntryPoint string `description:"Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router." json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty" export:"true"`
|
||||||
|
TLS *TLS `description:"TLS configuration for mTLS communication between Traefik and Hub Agent." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
|
||||||
|
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS configures the mTLS connection between Traefik Proxy and the Traefik Hub Agent.
|
||||||
|
type TLS struct {
|
||||||
|
Insecure bool `description:"Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
|
||||||
|
CA ttls.FileOrContent `description:"The certificate authority authenticates the Traefik Hub Agent certificate." json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty" loggable:"false"`
|
||||||
|
Cert ttls.FileOrContent `description:"The TLS certificate for Traefik Proxy as a TLS client." json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty" loggable:"false"`
|
||||||
|
Key ttls.FileOrContent `description:"The TLS key for Traefik Proxy as a TLS client." json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values.
|
||||||
|
func (p *Provider) SetDefaults() {
|
||||||
|
p.EntryPoint = DefaultEntryPointName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the provider.
|
||||||
|
func (p *Provider) Init() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide allows the hub provider to provide configurations to traefik using the given configuration channel.
|
||||||
|
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listener: %w", err)
|
||||||
|
}
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
|
||||||
|
client, err := createAgentClient(p.TLS)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating Hub Agent HTTP client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.server = &http.Server{Handler: newHandler(p.EntryPoint, port, configurationChan, p.TLS, client)}
|
||||||
|
|
||||||
|
// TODO: this is going to be leaky (because no context to make it terminate)
|
||||||
|
// if/when Provide lifecycle differs with Traefik lifecycle.
|
||||||
|
go func() {
|
||||||
|
if err = p.server.Serve(listener); err != nil {
|
||||||
|
log.WithoutContext().WithField(log.ProviderName, "hub").Errorf("Unexpected error while running server: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
exposeAPIAndMetrics(configurationChan, p.EntryPoint, port, p.TLS)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exposeAPIAndMetrics(cfgChan chan<- dynamic.Message, ep string, port int, tlsCfg *TLS) {
|
||||||
|
cfg := emptyDynamicConfiguration()
|
||||||
|
|
||||||
|
patchDynamicConfiguration(cfg, ep, port, tlsCfg)
|
||||||
|
|
||||||
|
cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchDynamicConfiguration(cfg *dynamic.Configuration, ep string, port int, tlsCfg *TLS) {
|
||||||
|
cfg.HTTP.Routers["traefik-hub-agent-api"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{ep},
|
||||||
|
Service: "api@internal",
|
||||||
|
Rule: "Host(`proxy.traefik`) && PathPrefix(`/api`)",
|
||||||
|
}
|
||||||
|
cfg.HTTP.Routers["traefik-hub-agent-metrics"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{ep},
|
||||||
|
Service: "prometheus@internal",
|
||||||
|
Rule: "Host(`proxy.traefik`) && PathPrefix(`/metrics`)",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HTTP.Routers["traefik-hub-agent-service"] = &dynamic.Router{
|
||||||
|
EntryPoints: []string{ep},
|
||||||
|
Service: "traefik-hub-agent-service",
|
||||||
|
Rule: "Host(`proxy.traefik`) && PathPrefix(`/config`, `/discover-ip`, `/state`)",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.HTTP.Services["traefik-hub-agent-service"] = &dynamic.Service{
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: fmt.Sprintf("http://127.0.0.1:%d", port),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsCfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsCfg.Insecure {
|
||||||
|
cfg.TLS.Options["traefik-hub"] = ttls.Options{
|
||||||
|
MinVersion: "VersionTLS13",
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.TLS.Options["traefik-hub"] = ttls.Options{
|
||||||
|
ClientAuth: ttls.ClientAuth{
|
||||||
|
CAFiles: []ttls.FileOrContent{tlsCfg.CA},
|
||||||
|
ClientAuthType: "RequireAndVerifyClientCert",
|
||||||
|
},
|
||||||
|
SniStrict: true,
|
||||||
|
MinVersion: "VersionTLS13",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.TLS.Certificates = append(cfg.TLS.Certificates, &ttls.CertAndStores{
|
||||||
|
Certificate: ttls.Certificate{
|
||||||
|
CertFile: tlsCfg.Cert,
|
||||||
|
KeyFile: tlsCfg.Key,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptyDynamicConfiguration() *dynamic.Configuration {
|
||||||
|
return &dynamic.Configuration{
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Routers: make(map[string]*dynamic.Router),
|
||||||
|
Middlewares: make(map[string]*dynamic.Middleware),
|
||||||
|
Services: make(map[string]*dynamic.Service),
|
||||||
|
ServersTransports: make(map[string]*dynamic.ServersTransport),
|
||||||
|
},
|
||||||
|
TCP: &dynamic.TCPConfiguration{
|
||||||
|
Routers: make(map[string]*dynamic.TCPRouter),
|
||||||
|
Services: make(map[string]*dynamic.TCPService),
|
||||||
|
},
|
||||||
|
TLS: &dynamic.TLSConfiguration{
|
||||||
|
Stores: make(map[string]ttls.Store),
|
||||||
|
Options: make(map[string]ttls.Options),
|
||||||
|
},
|
||||||
|
UDP: &dynamic.UDPConfiguration{
|
||||||
|
Routers: make(map[string]*dynamic.UDPRouter),
|
||||||
|
Services: make(map[string]*dynamic.UDPService),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAgentClient(tlsCfg *TLS) (http.Client, error) {
|
||||||
|
var client http.Client
|
||||||
|
if tlsCfg.Insecure {
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
caContent, err := tlsCfg.CA.Read()
|
||||||
|
if err != nil {
|
||||||
|
return client, fmt.Errorf("reading CA: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
if ok := roots.AppendCertsFromPEM(caContent); !ok {
|
||||||
|
return client, errors.New("appending CA error")
|
||||||
|
}
|
||||||
|
|
||||||
|
certContent, err := tlsCfg.Cert.Read()
|
||||||
|
if err != nil {
|
||||||
|
return client, fmt.Errorf("reading Cert: %w", err)
|
||||||
|
}
|
||||||
|
keyContent, err := tlsCfg.Key.Read()
|
||||||
|
if err != nil {
|
||||||
|
return client, fmt.Errorf("reading Key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certificate, err := tls.X509KeyPair(certContent, keyContent)
|
||||||
|
if err != nil {
|
||||||
|
return client, fmt.Errorf("creating key pair: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mTLS
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: roots,
|
||||||
|
Certificates: []tls.Certificate{certificate},
|
||||||
|
ServerName: "agent.traefik",
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
10
webui/src/statics/providers/hub.svg
Normal file
10
webui/src/statics/providers/hub.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g fill="#dfdfdf">
|
||||||
|
<path d="M128 0H0v128h128V0z"/>
|
||||||
|
<path d="M48 0H0v48h48V0zM128 0H80v48h48V0zM128 80H80v48h48V80zM48 80H0v48h48V80z"/>
|
||||||
|
</g>
|
||||||
|
<path d="M27.429 27.429h73.142v73.142H27.43V27.43z"/>
|
||||||
|
<path d="m65.323 31.205 16.89 9.164a2.905 2.905 0 0 1 1.516 2.557l-.014 14.763 15.34 8.323a2.905 2.905 0 0 1 1.516 2.558l-.016 16.47a2.905 2.905 0 0 1-1.51 2.55L82.17 96.792a2.873 2.873 0 0 1-2.776-.013l-15.279-8.512-15.63 8.525a2.873 2.873 0 0 1-2.775-.013l-16.794-9.356a2.906 2.906 0 0 1-1.488-2.539V68.606c0-1.063.577-2.041 1.505-2.55l15.337-8.403v-14.69c0-1.064.577-2.041 1.505-2.55l16.794-9.202a2.873 2.873 0 0 1 2.753-.006zM79.888 63.21l-11.997 6.57a1.937 1.937 0 0 0-1.004 1.7v10.554c0 .704.38 1.352.992 1.693l12.006 6.688c.575.32 1.273.324 1.85.009l12.042-6.566a1.937 1.937 0 0 0 1.007-1.7l.01-10.703c0-.712-.388-1.367-1.01-1.705l-12.061-6.544a1.916 1.916 0 0 0-1.835.004zm-33.69 0-11.991 6.57a1.937 1.937 0 0 0-1.004 1.7v10.554c0 .704.38 1.352.992 1.693l12.006 6.688c.575.32 1.273.324 1.85.009l12.041-6.566a1.937 1.937 0 0 0 1.007-1.7l.01-10.672c.001-.71-.385-1.365-1.007-1.703l-12.066-6.576a1.916 1.916 0 0 0-1.838.003zm16.844-25.645L51.05 44.137a1.937 1.937 0 0 0-1.004 1.699v10.552c0 .705.381 1.355.996 1.695l12.036 6.673c.575.319 1.271.321 1.848.007l12.01-6.554a1.937 1.937 0 0 0 1.007-1.699l.01-10.699c0-.712-.388-1.367-1.01-1.705l-12.065-6.545a1.916 1.916 0 0 0-1.835.004z" fill="#7F8C2B" fill-rule="nonzero"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
Loading…
Reference in a new issue