From 2c6418e17ac6580b4726e08c70132b1c540f92c1 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:14:06 +0200 Subject: [PATCH 01/30] docs: fix typo and improve explanation on internal resources --- docs/content/migration/v2-to-v3.md | 4 ++-- docs/content/observability/access-logs.md | 4 ++-- docs/content/observability/metrics/overview.md | 2 +- docs/content/observability/tracing/overview.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index 6fb88c0e7..61a60d990 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -724,7 +724,7 @@ Here are two possible transition strategies: Please check the [OpenTelemetry Tracing provider documention](../observability/tracing/opentelemetry.md) for more information. -#### Internal Resources Observability (AccessLogs, Metrics and Tracing) +#### Internal Resources Observability In v3, observability for internal routers or services (e.g.: `ping@internal`) is disabled by default. To enable it one should use the new `addInternals` option for AccessLogs, Metrics or Tracing. @@ -732,4 +732,4 @@ Please take a look at the observability documentation for more information: - [AccessLogs](../observability/access-logs.md#addinternals) - [Metrics](../observability/metrics/overview.md#addinternals) -- [AccessLogs](../observability/tracing/overview.md#addinternals) +- [Tracing](../observability/tracing/overview.md#addinternals) diff --git a/docs/content/observability/access-logs.md b/docs/content/observability/access-logs.md index beb114db1..2105cc030 100644 --- a/docs/content/observability/access-logs.md +++ b/docs/content/observability/access-logs.md @@ -30,7 +30,7 @@ accessLog: {} _Optional, Default="false"_ -Enables accessLogs for internal resources. +Enables accessLogs for internal resources (e.g.: `ping@internal`). ```yaml tab="File (YAML)" accesslog: @@ -187,7 +187,7 @@ accessLog: [accessLog.fields] defaultMode = "keep" - + [accessLog.fields.names] "ClientUsername" = "drop" diff --git a/docs/content/observability/metrics/overview.md b/docs/content/observability/metrics/overview.md index 082d4aed0..a49c5053c 100644 --- a/docs/content/observability/metrics/overview.md +++ b/docs/content/observability/metrics/overview.md @@ -21,7 +21,7 @@ and [Kubernetes](https://grafana.com/grafana/dashboards/17347) deployments. _Optional, Default="false"_ -Enables metrics for internal resources. +Enables metrics for internal resources (e.g.: `ping@internals`). ```yaml tab="File (YAML)" metrics: diff --git a/docs/content/observability/tracing/overview.md b/docs/content/observability/tracing/overview.md index 72dd20cd6..e81f63edf 100644 --- a/docs/content/observability/tracing/overview.md +++ b/docs/content/observability/tracing/overview.md @@ -36,7 +36,7 @@ tracing: {} _Optional, Default="false"_ -Enables tracing for internal resources. +Enables tracing for internal resources (e.g.: `ping@internal`). ```yaml tab="File (YAML)" tracing: @@ -159,4 +159,4 @@ tracing: ```bash tab="CLI" --tracing.capturedResponseHeaders[0]=X-CustomHeader -``` \ No newline at end of file +``` From d3516aec313c35faeb89a2f35f3b3532d19c7ccb Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:32:05 +0200 Subject: [PATCH 02/30] docs: excludedIPs with IPWhiteList and IPAllowList middleware --- docs/content/middlewares/http/ipallowlist.md | 63 +++++++++++++++++-- docs/content/middlewares/http/ipwhitelist.md | 19 +++++- docs/content/middlewares/tcp/ipallowlist.md | 2 +- .../kubernetes-crd-definition-v1.yml | 12 ++-- .../traefik.containo.us_middlewares.yaml | 6 +- .../traefik.io_middlewares.yaml | 6 +- integration/fixtures/k8s/01-traefik-crd.yml | 12 ++-- pkg/config/dynamic/middlewares.go | 6 +- pkg/config/dynamic/tcp_middlewares.go | 5 ++ 9 files changed, 100 insertions(+), 31 deletions(-) diff --git a/docs/content/middlewares/http/ipallowlist.md b/docs/content/middlewares/http/ipallowlist.md index d62e253bb..ec7d70e03 100644 --- a/docs/content/middlewares/http/ipallowlist.md +++ b/docs/content/middlewares/http/ipallowlist.md @@ -8,11 +8,11 @@ description: "Learn how to use IPAllowList in HTTP middleware for limiting clien Limiting Clients to Specific IPs {: .subtitle } -IPAllowList accepts / refuses requests based on the client IP. +IPAllowList limits allowed requests based on the client IP. ## Configuration Examples -```yaml tab="Docker & Swarm" +```yaml tab="Docker" # Accepts request from defined IP labels: - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" @@ -35,6 +35,18 @@ spec: - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" ``` +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange": "127.0.0.1/32,192.168.1.7" +} +``` + +```yaml tab="Rancher" +# Accepts request from defined IP +labels: + - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" +``` + ```yaml tab="File (YAML)" # Accepts request from defined IP http: @@ -57,6 +69,8 @@ http: ### `sourceRange` +_Required_ + The `sourceRange` option sets the allowed IPs (or ranges of allowed IPs by using CIDR notation). ### `ipStrategy` @@ -83,7 +97,7 @@ The `depth` option tells Traefik to use the `X-Forwarded-For` header and take th | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `3` | `"11.0.0.1"` | | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `5` | `""` | -```yaml tab="Docker & Swarm" +```yaml tab="Docker" # Allowlisting Based on `X-Forwarded-For` with `depth=2` labels: - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" @@ -111,6 +125,20 @@ spec: - "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.depth=2" ``` +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange": "127.0.0.1/32, 192.168.1.7", + "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.depth": "2" +} +``` + +```yaml tab="Rancher" +# Whitelisting Based on `X-Forwarded-For` with `depth=2` +labels: + - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourcerange=127.0.0.1/32, 192.168.1.7" + - "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.depth=2" +``` + ```yaml tab="File (YAML)" # Allowlisting Based on `X-Forwarded-For` with `depth=2` http: @@ -149,9 +177,10 @@ http: | `"10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1"` | `"15.0.0.1,16.0.0.1"` | `"13.0.0.1"` | | `"10.0.0.1,11.0.0.1"` | `"10.0.0.1,11.0.0.1"` | `""` | -```yaml tab="Docker & Swarm" +```yaml tab="Docker" # Exclude from `X-Forwarded-For` labels: + - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourceRange=127.0.0.1/32, 192.168.1.0/24" - "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" ``` @@ -163,6 +192,9 @@ metadata: name: test-ipallowlist spec: ipAllowList: + sourceRange: + - 127.0.0.1/32 + - 192.168.1.0/24 ipStrategy: excludedIPs: - 127.0.0.1/32 @@ -171,25 +203,44 @@ spec: ```yaml tab="Consul Catalog" # Exclude from `X-Forwarded-For` +- "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourceRange=127.0.0.1/32, 192.168.1.0/24" - "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" ``` +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourceRange=127.0.0.1/32, 192.168.1.0/24" + "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips": "127.0.0.1/32, 192.168.1.7" +} +``` + +```yaml tab="Rancher" +# Exclude from `X-Forwarded-For` +labels: + - "traefik.http.middlewares.test-ipallowlist.ipallowlist.sourceRange=127.0.0.1/32, 192.168.1.0/24" + - "traefik.http.middlewares.test-ipallowlist.ipallowlist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" +``` + ```yaml tab="File (YAML)" # Exclude from `X-Forwarded-For` http: middlewares: test-ipallowlist: ipAllowList: + sourceRange: + - 127.0.0.1/32 + - 192.168.1.0/24 ipStrategy: excludedIPs: - - "127.0.0.1/32" - - "192.168.1.7" + - 127.0.0.1/32 + - 192.168.1.7 ``` ```toml tab="File (TOML)" # Exclude from `X-Forwarded-For` [http.middlewares] [http.middlewares.test-ipallowlist.ipAllowList] + sourceRange = ["127.0.0.1/32", "192.168.1.0/24"] [http.middlewares.test-ipallowlist.ipAllowList.ipStrategy] excludedIPs = ["127.0.0.1/32", "192.168.1.7"] ``` diff --git a/docs/content/middlewares/http/ipwhitelist.md b/docs/content/middlewares/http/ipwhitelist.md index eaf761541..13ca1c0f0 100644 --- a/docs/content/middlewares/http/ipwhitelist.md +++ b/docs/content/middlewares/http/ipwhitelist.md @@ -10,7 +10,7 @@ Limiting Clients to Specific IPs ![IPWhiteList](../../assets/img/middleware/ipwhitelist.png) -IPWhiteList accepts / refuses requests based on the client IP. +IPWhiteList limits allowed requests based on the client IP. !!! warning @@ -75,6 +75,8 @@ http: ### `sourceRange` +_Required_ + The `sourceRange` option sets the allowed IPs (or ranges of allowed IPs by using CIDR notation). ### `ipStrategy` @@ -184,6 +186,7 @@ http: ```yaml tab="Docker" # Exclude from `X-Forwarded-For` labels: + - "traefik.http.middlewares.test-ipwhitelist.ipwhitelist.sourceRange=127.0.0.1/32, 192.168.1.0/24" - "traefik.http.middlewares.test-ipwhitelist.ipwhitelist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" ``` @@ -196,6 +199,9 @@ metadata: spec: ipWhiteList: ipStrategy: + sourceRange: + - 127.0.0.1/32 + - 192.168.1.0/24 excludedIPs: - 127.0.0.1/32 - 192.168.1.7 @@ -203,11 +209,13 @@ spec: ```yaml tab="Consul Catalog" # Exclude from `X-Forwarded-For` +- "traefik.http.middlewares.test-ipwhitelist.ipwhitelist.sourceRange=127.0.0.1/32, 192.168.1.0/24" - "traefik.http.middlewares.test-ipwhitelist.ipwhitelist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" ``` ```json tab="Marathon" "labels": { + "traefik.http.middlewares.test-ipwhitelist.ipwhitelist.sourceRange=127.0.0.1/32, 192.168.1.0/24" "traefik.http.middlewares.test-ipwhitelist.ipwhitelist.ipstrategy.excludedips": "127.0.0.1/32, 192.168.1.7" } ``` @@ -215,6 +223,7 @@ spec: ```yaml tab="Rancher" # Exclude from `X-Forwarded-For` labels: + - "traefik.http.middlewares.test-ipwhitelist.ipwhitelist.sourceRange=127.0.0.1/32, 192.168.1.0/24" - "traefik.http.middlewares.test-ipwhitelist.ipwhitelist.ipstrategy.excludedips=127.0.0.1/32, 192.168.1.7" ``` @@ -224,16 +233,20 @@ http: middlewares: test-ipwhitelist: ipWhiteList: + sourceRange: + - 127.0.0.1/32 + - 192.168.1.0/24 ipStrategy: excludedIPs: - - "127.0.0.1/32" - - "192.168.1.7" + - 127.0.0.1/32 + - 192.168.1.7 ``` ```toml tab="File (TOML)" # Exclude from `X-Forwarded-For` [http.middlewares] [http.middlewares.test-ipwhitelist.ipWhiteList] + sourceRange = ["127.0.0.1/32", "192.168.1.0/24"] [http.middlewares.test-ipwhitelist.ipWhiteList.ipStrategy] excludedIPs = ["127.0.0.1/32", "192.168.1.7"] ``` diff --git a/docs/content/middlewares/tcp/ipallowlist.md b/docs/content/middlewares/tcp/ipallowlist.md index e8466b94e..39e014ef7 100644 --- a/docs/content/middlewares/tcp/ipallowlist.md +++ b/docs/content/middlewares/tcp/ipallowlist.md @@ -8,7 +8,7 @@ description: "Learn how to use IPAllowList in TCP middleware for limiting client Limiting Clients to Specific IPs {: .subtitle } -IPAllowList accepts / refuses connections based on the client IP. +IPAllowList limits allowed requests based on the client IP. ## Configuration Examples diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 85ff27f7e..2b4b4aad6 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1241,7 +1241,7 @@ spec: ipAllowList: description: |- IPAllowList holds the IP allowlist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipallowlist/ properties: ipStrategy: @@ -1271,7 +1271,7 @@ spec: ipWhiteList: description: |- IPWhiteList holds the IP whitelist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipwhitelist/ Deprecated: please use IPAllowList instead. properties: @@ -1294,7 +1294,7 @@ spec: type: object sourceRange: description: SourceRange defines the set of allowed IPs (or ranges - of allowed IPs by using CIDR notation). + of allowed IPs by using CIDR notation). Required. items: type: string type: array @@ -3671,7 +3671,7 @@ spec: ipAllowList: description: |- IPAllowList holds the IP allowlist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipallowlist/ properties: ipStrategy: @@ -3701,7 +3701,7 @@ spec: ipWhiteList: description: |- IPWhiteList holds the IP whitelist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipwhitelist/ Deprecated: please use IPAllowList instead. properties: @@ -3724,7 +3724,7 @@ spec: type: object sourceRange: description: SourceRange defines the set of allowed IPs (or ranges - of allowed IPs by using CIDR notation). + of allowed IPs by using CIDR notation). Required. items: type: string type: array diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml index be0af55c5..605b8af5f 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml @@ -626,7 +626,7 @@ spec: ipAllowList: description: |- IPAllowList holds the IP allowlist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipallowlist/ properties: ipStrategy: @@ -656,7 +656,7 @@ spec: ipWhiteList: description: |- IPWhiteList holds the IP whitelist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipwhitelist/ Deprecated: please use IPAllowList instead. properties: @@ -679,7 +679,7 @@ spec: type: object sourceRange: description: SourceRange defines the set of allowed IPs (or ranges - of allowed IPs by using CIDR notation). + of allowed IPs by using CIDR notation). Required. items: type: string type: array diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 66913e653..0068a365f 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -626,7 +626,7 @@ spec: ipAllowList: description: |- IPAllowList holds the IP allowlist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipallowlist/ properties: ipStrategy: @@ -656,7 +656,7 @@ spec: ipWhiteList: description: |- IPWhiteList holds the IP whitelist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipwhitelist/ Deprecated: please use IPAllowList instead. properties: @@ -679,7 +679,7 @@ spec: type: object sourceRange: description: SourceRange defines the set of allowed IPs (or ranges - of allowed IPs by using CIDR notation). + of allowed IPs by using CIDR notation). Required. items: type: string type: array diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 85ff27f7e..2b4b4aad6 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1241,7 +1241,7 @@ spec: ipAllowList: description: |- IPAllowList holds the IP allowlist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipallowlist/ properties: ipStrategy: @@ -1271,7 +1271,7 @@ spec: ipWhiteList: description: |- IPWhiteList holds the IP whitelist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipwhitelist/ Deprecated: please use IPAllowList instead. properties: @@ -1294,7 +1294,7 @@ spec: type: object sourceRange: description: SourceRange defines the set of allowed IPs (or ranges - of allowed IPs by using CIDR notation). + of allowed IPs by using CIDR notation). Required. items: type: string type: array @@ -3671,7 +3671,7 @@ spec: ipAllowList: description: |- IPAllowList holds the IP allowlist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipallowlist/ properties: ipStrategy: @@ -3701,7 +3701,7 @@ spec: ipWhiteList: description: |- IPWhiteList holds the IP whitelist middleware configuration. - This middleware accepts / refuses requests based on the client IP. + This middleware limits allowed requests based on the client IP. More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipwhitelist/ Deprecated: please use IPAllowList instead. properties: @@ -3724,7 +3724,7 @@ spec: type: object sourceRange: description: SourceRange defines the set of allowed IPs (or ranges - of allowed IPs by using CIDR notation). + of allowed IPs by using CIDR notation). Required. items: type: string type: array diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 391f22de4..c820d9fcf 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -387,11 +387,11 @@ func (s *IPStrategy) Get() (ip.Strategy, error) { // +k8s:deepcopy-gen=true // IPWhiteList holds the IP whitelist middleware configuration. -// This middleware accepts / refuses requests based on the client IP. +// This middleware limits allowed requests based on the client IP. // More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipwhitelist/ // Deprecated: please use IPAllowList instead. type IPWhiteList struct { - // SourceRange defines the set of allowed IPs (or ranges of allowed IPs by using CIDR notation). + // SourceRange defines the set of allowed IPs (or ranges of allowed IPs by using CIDR notation). Required. SourceRange []string `json:"sourceRange,omitempty" toml:"sourceRange,omitempty" yaml:"sourceRange,omitempty"` IPStrategy *IPStrategy `json:"ipStrategy,omitempty" toml:"ipStrategy,omitempty" yaml:"ipStrategy,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` } @@ -399,7 +399,7 @@ type IPWhiteList struct { // +k8s:deepcopy-gen=true // IPAllowList holds the IP allowlist middleware configuration. -// This middleware accepts / refuses requests based on the client IP. +// This middleware limits allowed requests based on the client IP. // More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/ipallowlist/ type IPAllowList struct { // SourceRange defines the set of allowed IPs (or ranges of allowed IPs by using CIDR notation). diff --git a/pkg/config/dynamic/tcp_middlewares.go b/pkg/config/dynamic/tcp_middlewares.go index bf6dbdb25..e98390c64 100644 --- a/pkg/config/dynamic/tcp_middlewares.go +++ b/pkg/config/dynamic/tcp_middlewares.go @@ -24,6 +24,9 @@ type TCPInFlightConn struct { // +k8s:deepcopy-gen=true // TCPIPWhiteList holds the TCP IPWhiteList middleware configuration. +// This middleware limits allowed requests based on the client IP. +// More info: https://doc.traefik.io/traefik/v2.11/middlewares/tcp/ipwhitelist/ +// Deprecated: please use IPAllowList instead. type TCPIPWhiteList struct { // SourceRange defines the allowed IPs (or ranges of allowed IPs by using CIDR notation). SourceRange []string `json:"sourceRange,omitempty" toml:"sourceRange,omitempty" yaml:"sourceRange,omitempty"` @@ -32,6 +35,8 @@ type TCPIPWhiteList struct { // +k8s:deepcopy-gen=true // TCPIPAllowList holds the TCP IPAllowList middleware configuration. +// This middleware limits allowed requests based on the client IP. +// More info: https://doc.traefik.io/traefik/v2.11/middlewares/tcp/ipallowlist/ type TCPIPAllowList struct { // SourceRange defines the allowed IPs (or ranges of allowed IPs by using CIDR notation). SourceRange []string `json:"sourceRange,omitempty" toml:"sourceRange,omitempty" yaml:"sourceRange,omitempty"` From ac1753a614c0105b3f73efa4a17cdf45aca37347 Mon Sep 17 00:00:00 2001 From: chrispruitt Date: Thu, 4 Apr 2024 05:54:04 -0400 Subject: [PATCH 03/30] Nomad provider to allow empty services --- docs/content/providers/nomad.md | 24 ++ .../reference/static-configuration/cli-ref.md | 3 + .../reference/static-configuration/env-ref.md | 3 + .../reference/static-configuration/file.toml | 1 + .../reference/static-configuration/file.yaml | 1 + pkg/provider/nomad/config.go | 21 +- pkg/provider/nomad/config_test.go | 337 +++++++++++++++ .../job_job1_WithGroupService_Scaling1.json | 220 ++++++++++ .../job_job2_WithGroupService_Scaling0.json | 220 ++++++++++ ...job3_WithGroupService_ScalingDisabled.json | 206 ++++++++++ ...hGroupService_ScalingDisabled_Stopped.json | 206 ++++++++++ ...ob_job5_WithGroupTaskService_Scaling1.json | 299 ++++++++++++++ ...ob_job6_WithGroupTaskService_Scaling0.json | 299 ++++++++++++++ pkg/provider/nomad/fixtures/job_job7_TCP.json | 222 ++++++++++ pkg/provider/nomad/fixtures/job_job8_UDP.json | 221 ++++++++++ .../job_job9_ScalingEnabled_Stopped.json | 220 ++++++++++ pkg/provider/nomad/fixtures/jobs_job1.json | 56 +++ pkg/provider/nomad/fixtures/jobs_job2.json | 47 +++ pkg/provider/nomad/fixtures/jobs_job3.json | 47 +++ pkg/provider/nomad/fixtures/jobs_job4.json | 47 +++ pkg/provider/nomad/fixtures/jobs_job5.json | 56 +++ pkg/provider/nomad/fixtures/jobs_job6.json | 47 +++ pkg/provider/nomad/fixtures/jobs_job7.json | 47 +++ pkg/provider/nomad/fixtures/jobs_job8.json | 47 +++ pkg/provider/nomad/fixtures/jobs_job9.json | 47 +++ .../nomad/fixtures/service_hello.json | 20 + pkg/provider/nomad/fixtures/service_job1.json | 18 + pkg/provider/nomad/fixtures/service_job2.json | 1 + pkg/provider/nomad/fixtures/service_job3.json | 18 + pkg/provider/nomad/fixtures/service_job4.json | 1 + .../nomad/fixtures/service_job5task1.json | 18 + .../nomad/fixtures/service_job5task2.json | 18 + .../nomad/fixtures/service_job6task1.json | 1 + .../nomad/fixtures/service_job6task2.json | 1 + pkg/provider/nomad/fixtures/service_job7.json | 20 + pkg/provider/nomad/fixtures/service_job8.json | 19 + pkg/provider/nomad/fixtures/service_job9.json | 1 + .../nomad/fixtures/service_redis.json | 18 + pkg/provider/nomad/fixtures/services.json | 21 + pkg/provider/nomad/nomad.go | 124 +++++- pkg/provider/nomad/nomad_test.go | 384 ++++++++++++++---- 41 files changed, 3540 insertions(+), 87 deletions(-) create mode 100644 pkg/provider/nomad/fixtures/job_job1_WithGroupService_Scaling1.json create mode 100644 pkg/provider/nomad/fixtures/job_job2_WithGroupService_Scaling0.json create mode 100644 pkg/provider/nomad/fixtures/job_job3_WithGroupService_ScalingDisabled.json create mode 100644 pkg/provider/nomad/fixtures/job_job4_WithGroupService_ScalingDisabled_Stopped.json create mode 100644 pkg/provider/nomad/fixtures/job_job5_WithGroupTaskService_Scaling1.json create mode 100644 pkg/provider/nomad/fixtures/job_job6_WithGroupTaskService_Scaling0.json create mode 100644 pkg/provider/nomad/fixtures/job_job7_TCP.json create mode 100644 pkg/provider/nomad/fixtures/job_job8_UDP.json create mode 100644 pkg/provider/nomad/fixtures/job_job9_ScalingEnabled_Stopped.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job1.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job2.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job3.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job4.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job5.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job6.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job7.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job8.json create mode 100644 pkg/provider/nomad/fixtures/jobs_job9.json create mode 100644 pkg/provider/nomad/fixtures/service_hello.json create mode 100644 pkg/provider/nomad/fixtures/service_job1.json create mode 100644 pkg/provider/nomad/fixtures/service_job2.json create mode 100644 pkg/provider/nomad/fixtures/service_job3.json create mode 100644 pkg/provider/nomad/fixtures/service_job4.json create mode 100644 pkg/provider/nomad/fixtures/service_job5task1.json create mode 100644 pkg/provider/nomad/fixtures/service_job5task2.json create mode 100644 pkg/provider/nomad/fixtures/service_job6task1.json create mode 100644 pkg/provider/nomad/fixtures/service_job6task2.json create mode 100644 pkg/provider/nomad/fixtures/service_job7.json create mode 100644 pkg/provider/nomad/fixtures/service_job8.json create mode 100644 pkg/provider/nomad/fixtures/service_job9.json create mode 100644 pkg/provider/nomad/fixtures/service_redis.json create mode 100644 pkg/provider/nomad/fixtures/services.json diff --git a/docs/content/providers/nomad.md b/docs/content/providers/nomad.md index f1d0d1fe1..85b3ac033 100644 --- a/docs/content/providers/nomad.md +++ b/docs/content/providers/nomad.md @@ -511,3 +511,27 @@ providers: --providers.nomad.namespaces=ns1,ns2 # ... ``` + +### `allowEmptyServices` + +_Optional, Default: false_ + +If the parameter is set to `true`, +it allows the creation of an empty [servers load balancer](../routing/services/index.md#servers-load-balancer) if the targeted Nomad service has no endpoints available. This results in a `503` HTTP response instead of a `404`. + +```yaml tab="File (YAML)" +providers: + nomad: + allowEmptyServices: true + # ... +``` + +```toml tab="File (TOML)" +[providers.nomad] + allowEmptyServices = true + # ... +``` + +```bash tab="CLI" +--providers.nomad.allowEmptyServices=true +``` diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index a632402ad..6194dfbfe 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -789,6 +789,9 @@ Kubernetes bearer token (not needed for in-cluster client). It accepts either a `--providers.nomad`: Enable Nomad backend with default settings. (Default: ```false```) +`--providers.nomad.allowemptyservices`: +Allow the creation of services without endpoints. (Default: ```false```) + `--providers.nomad.constraints`: Constraints is an expression that Traefik matches against the Nomad service's tags to determine whether to create route(s) for that service. diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 3fe8614e5..7fcde07fc 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -789,6 +789,9 @@ Kubernetes bearer token (not needed for in-cluster client). It accepts either a `TRAEFIK_PROVIDERS_NOMAD`: Enable Nomad backend with default settings. (Default: ```false```) +`TRAEFIK_PROVIDERS_NOMAD_ALLOWEMPTYSERVICES`: +Allow the creation of services without endpoints. (Default: ```false```) + `TRAEFIK_PROVIDERS_NOMAD_CONSTRAINTS`: Constraints is an expression that Traefik matches against the Nomad service's tags to determine whether to create route(s) for that service. diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 6bf62c9cf..e5a6e379e 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -185,6 +185,7 @@ stale = true exposedByDefault = true refreshInterval = "42s" + allowEmptyServices = true namespaces = ["foobar", "foobar"] [providers.nomad.endpoint] address = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index d54e06b60..362418fbc 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -216,6 +216,7 @@ providers: stale: true exposedByDefault: true refreshInterval: 42s + allowEmptyServices: true namespaces: - foobar - foobar diff --git a/pkg/provider/nomad/config.go b/pkg/provider/nomad/config.go index b6caa8cf1..83022f31d 100644 --- a/pkg/provider/nomad/config.go +++ b/pkg/provider/nomad/config.go @@ -97,8 +97,11 @@ func (p *Provider) buildTCPConfig(i item, configuration *dynamic.TCPConfiguratio } for _, service := range configuration.Services { - if err := p.addServerTCP(i, service.LoadBalancer); err != nil { - return err + // Leave load balancer empty when no address and allowEmptyServices = true + if !(i.Address == "" && p.AllowEmptyServices) { + if err := p.addServerTCP(i, service.LoadBalancer); err != nil { + return err + } } } @@ -115,8 +118,11 @@ func (p *Provider) buildUDPConfig(i item, configuration *dynamic.UDPConfiguratio } for _, service := range configuration.Services { - if err := p.addServerUDP(i, service.LoadBalancer); err != nil { - return err + // Leave load balancer empty when no address and allowEmptyServices = true + if !(i.Address == "" && p.AllowEmptyServices) { + if err := p.addServerUDP(i, service.LoadBalancer); err != nil { + return err + } } } @@ -136,8 +142,11 @@ func (p *Provider) buildServiceConfig(i item, configuration *dynamic.HTTPConfigu } for _, service := range configuration.Services { - if err := p.addServer(i, service.LoadBalancer); err != nil { - return err + // Leave load balancer empty when no address and allowEmptyServices = true + if !(i.Address == "" && p.AllowEmptyServices) { + if err := p.addServer(i, service.LoadBalancer); err != nil { + return err + } } } diff --git a/pkg/provider/nomad/config_test.go b/pkg/provider/nomad/config_test.go index e1ec061e5..2f036e929 100644 --- a/pkg/provider/nomad/config_test.go +++ b/pkg/provider/nomad/config_test.go @@ -706,6 +706,42 @@ func Test_buildConfig(t *testing.T) { }, }, }, + { + desc: "empty service", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.enable=true", + }, + Address: "", + Port: -1, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Stores: map[string]tls.Store{}, + }, + }, + }, { desc: "one service with rule label", items: []item{ @@ -2825,6 +2861,307 @@ func Test_buildConfig(t *testing.T) { } } +func Test_buildConfigAllowEmptyServicesTrue(t *testing.T) { + testCases := []struct { + desc string + items []item + constraints string + expected *dynamic.Configuration + }{ + { + desc: "empty service http", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.enable=true", + }, + Address: "", + Port: -1, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "Test": { + Service: "Test", + Rule: "Host(`Test.traefik.test`)", + DefaultRule: true, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "Test": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: nil, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Stores: map[string]tls.Store{}, + }, + }, + }, + { + desc: "empty service tcp", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.routers.test.rule = HostSNI(`foobar`)", + }, + Address: "", + Port: -1, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "test": { + Rule: "HostSNI(`foobar`)", + Service: "Test", + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "Test": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{}, + }, + }, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Stores: map[string]tls.Store{}, + }, + }, + }, + { + desc: "empty service udp", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.udp.routers.test.entrypoints = udp", + }, + Address: "", + Port: -1, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "test": { + EntryPoints: []string{"udp"}, + Service: "Test", + }, + }, + Services: map[string]*dynamic.UDPService{ + "Test": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{}, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Stores: map[string]tls.Store{}, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + p := new(Provider) + p.SetDefaults() + p.AllowEmptyServices = true + p.DefaultRule = "Host(`{{ normalize .Name }}.traefik.test`)" + p.Constraints = test.constraints + err := p.Init() + require.NoError(t, err) + + ctx := context.TODO() + c := p.buildConfig(ctx, test.items) + require.Equal(t, test.expected, c) + }) + } +} + +func Test_buildConfigAllowEmptyServicesFalseDefault(t *testing.T) { + testCases := []struct { + desc string + items []item + constraints string + expected *dynamic.Configuration + }{ + { + desc: "empty service http", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.enable=true", + }, + Address: "", + Port: -1, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Stores: map[string]tls.Store{}, + }, + }, + }, + { + desc: "empty service tcp", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.tcp.routers.test.rule = HostSNI(`foobar`)", + }, + Address: "", + Port: -1, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Stores: map[string]tls.Store{}, + }, + }, + }, + { + desc: "empty service udp", + items: []item{ + { + ID: "id1", + Name: "Test", + Tags: []string{ + "traefik.udp.routers.test.entrypoints = udp", + }, + Address: "", + Port: -1, + ExtraConf: configuration{Enable: true}, + }, + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Stores: map[string]tls.Store{}, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + p := new(Provider) + p.SetDefaults() + p.DefaultRule = "Host(`{{ normalize .Name }}.traefik.test`)" + p.Constraints = test.constraints + err := p.Init() + require.NoError(t, err) + + ctx := context.TODO() + c := p.buildConfig(ctx, test.items) + require.Equal(t, test.expected, c) + }) + } +} + func Test_keepItem(t *testing.T) { testCases := []struct { name string diff --git a/pkg/provider/nomad/fixtures/job_job1_WithGroupService_Scaling1.json b/pkg/provider/nomad/fixtures/job_job1_WithGroupService_Scaling1.json new file mode 100644 index 000000000..9044736c6 --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job1_WithGroupService_Scaling1.json @@ -0,0 +1,220 @@ +{ + "Stop": false, + "Region": "global", + "Namespace": "default", + "ID": "job1", + "ParentID": "", + "Name": "job1", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 1, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": { + "ID": "654cb8ee-9c81-4fe8-02a0-2aecdea35ae3", + "Type": "horizontal", + "Target": { + "Job": "job1", + "Group": "group1", + "Namespace": "default" + }, + "Policy": null, + "Min": 0, + "Max": 3, + "Enabled": true, + "CreateIndex": 0, + "ModifyIndex": 0 + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "image": "nginx", + "ports": [ + "http" + ] + }, + "Env": null, + "Services": null, + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": [ + { + "Name": "job1", + "TaskName": "", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "running", + "StatusDescription": "", + "Stable": true, + "Version": 11, + "SubmitTime": 1705690395733241600, + "CreateIndex": 493, + "ModifyIndex": 9961, + "JobModifyIndex": 9955 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/job_job2_WithGroupService_Scaling0.json b/pkg/provider/nomad/fixtures/job_job2_WithGroupService_Scaling0.json new file mode 100644 index 000000000..0cf8656f7 --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job2_WithGroupService_Scaling0.json @@ -0,0 +1,220 @@ +{ + "Stop": false, + "Region": "global", + "Namespace": "default", + "ID": "job2", + "ParentID": "", + "Name": "job2", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 0, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": { + "ID": "0ae1d8fa-aa84-0b1b-941f-82dc71bc4664", + "Type": "horizontal", + "Target": { + "Group": "group1", + "Namespace": "default", + "Job": "job2" + }, + "Policy": null, + "Min": 0, + "Max": 3, + "Enabled": true, + "CreateIndex": 9975, + "ModifyIndex": 9975 + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "ports": [ + "http" + ], + "image": "nginx" + }, + "Env": null, + "Services": null, + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": [ + { + "Name": "job2", + "TaskName": "", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "dead", + "StatusDescription": "", + "Stable": true, + "Version": 10, + "SubmitTime": 1705690880440177400, + "CreateIndex": 2923, + "ModifyIndex": 10048, + "JobModifyIndex": 10044 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/job_job3_WithGroupService_ScalingDisabled.json b/pkg/provider/nomad/fixtures/job_job3_WithGroupService_ScalingDisabled.json new file mode 100644 index 000000000..a0efc5e16 --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job3_WithGroupService_ScalingDisabled.json @@ -0,0 +1,206 @@ +{ + "Stop": false, + "Region": "global", + "Namespace": "default", + "ID": "job3", + "ParentID": "", + "Name": "job3", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 1, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": null, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "ports": [ + "http" + ], + "image": "nginx" + }, + "Env": null, + "Services": null, + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": [ + { + "Name": "job3", + "TaskName": "", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "running", + "StatusDescription": "", + "Stable": true, + "Version": 4, + "SubmitTime": 1705690892484556300, + "CreateIndex": 2932, + "ModifyIndex": 9992, + "JobModifyIndex": 9983 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/job_job4_WithGroupService_ScalingDisabled_Stopped.json b/pkg/provider/nomad/fixtures/job_job4_WithGroupService_ScalingDisabled_Stopped.json new file mode 100644 index 000000000..55d312dcd --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job4_WithGroupService_ScalingDisabled_Stopped.json @@ -0,0 +1,206 @@ +{ + "Stop": true, + "Region": "global", + "Namespace": "default", + "ID": "job4", + "ParentID": "", + "Name": "job4", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 1, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": null, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "image": "nginx", + "ports": [ + "http" + ] + }, + "Env": null, + "Services": null, + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": [ + { + "Name": "job4", + "TaskName": "", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "dead", + "StatusDescription": "", + "Stable": true, + "Version": 11, + "SubmitTime": 1705690905317339000, + "CreateIndex": 3079, + "ModifyIndex": 10054, + "JobModifyIndex": 10052 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/job_job5_WithGroupTaskService_Scaling1.json b/pkg/provider/nomad/fixtures/job_job5_WithGroupTaskService_Scaling1.json new file mode 100644 index 000000000..f0171814d --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job5_WithGroupTaskService_Scaling1.json @@ -0,0 +1,299 @@ +{ + "Stop": false, + "Region": "global", + "Namespace": "default", + "ID": "job5", + "ParentID": "", + "Name": "job5", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 1, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": { + "ID": "56d57f83-7b9e-fbfa-af96-e2c7f5dc698c", + "Type": "horizontal", + "Target": { + "Namespace": "default", + "Job": "job5", + "Group": "group1" + }, + "Policy": null, + "Min": 0, + "Max": 3, + "Enabled": true, + "CreateIndex": 0, + "ModifyIndex": 0 + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "image": "nginx", + "ports": [ + "http" + ] + }, + "Env": null, + "Services": [ + { + "Name": "job5task1", + "TaskName": "task1", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + }, + { + "Name": "task2", + "Driver": "docker", + "User": "", + "Config": { + "ports": [ + "other" + ], + "image": "nginx" + }, + "Env": null, + "Services": [ + { + "Name": "job5task2", + "TaskName": "task2", + "PortLabel": "other", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + }, + { + "Label": "other", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": null, + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "running", + "StatusDescription": "", + "Stable": true, + "Version": 7, + "SubmitTime": 1705690918813110300, + "CreateIndex": 3095, + "ModifyIndex": 10018, + "JobModifyIndex": 10008 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/job_job6_WithGroupTaskService_Scaling0.json b/pkg/provider/nomad/fixtures/job_job6_WithGroupTaskService_Scaling0.json new file mode 100644 index 000000000..a4528e4dd --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job6_WithGroupTaskService_Scaling0.json @@ -0,0 +1,299 @@ +{ + "Stop": false, + "Region": "global", + "Namespace": "default", + "ID": "job6", + "ParentID": "", + "Name": "job6", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 0, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": { + "ID": "d18b02ae-5a68-e635-8100-06b110910b5f", + "Type": "horizontal", + "Target": { + "Job": "job6", + "Group": "group1", + "Namespace": "default" + }, + "Policy": null, + "Min": 0, + "Max": 3, + "Enabled": true, + "CreateIndex": 10020, + "ModifyIndex": 10020 + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "image": "nginx", + "ports": [ + "http" + ] + }, + "Env": null, + "Services": [ + { + "Name": "job6task1", + "TaskName": "task1", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + }, + { + "Name": "task2", + "Driver": "docker", + "User": "", + "Config": { + "image": "nginx", + "ports": [ + "other" + ] + }, + "Env": null, + "Services": [ + { + "Name": "job6task2", + "TaskName": "task2", + "PortLabel": "other", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + }, + { + "Label": "other", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": null, + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "dead", + "StatusDescription": "", + "Stable": true, + "Version": 3, + "SubmitTime": 1705690931854150700, + "CreateIndex": 9941, + "ModifyIndex": 10078, + "JobModifyIndex": 10074 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/job_job7_TCP.json b/pkg/provider/nomad/fixtures/job_job7_TCP.json new file mode 100644 index 000000000..3bf39a776 --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job7_TCP.json @@ -0,0 +1,222 @@ +{ + "Stop": false, + "Region": "global", + "Namespace": "default", + "ID": "job7", + "ParentID": "", + "Name": "job7", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 1, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": { + "ID": "e3b82307-1b7f-5fc6-901a-d9174418461f", + "Type": "horizontal", + "Target": { + "Namespace": "default", + "Job": "job7", + "Group": "group1" + }, + "Policy": null, + "Min": 0, + "Max": 3, + "Enabled": true, + "CreateIndex": 0, + "ModifyIndex": 0 + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "image": "nginx", + "ports": [ + "http" + ] + }, + "Env": null, + "Services": null, + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": [ + { + "Name": "job7", + "TaskName": "", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true", + "traefik.tcp.routers.job7.rule=HostSNI(`job7`)", + "traefik.tcp.routers.job7.tls.passthrough=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "running", + "StatusDescription": "", + "Stable": true, + "Version": 1, + "SubmitTime": 1705752438438572000, + "CreateIndex": 11221, + "ModifyIndex": 11238, + "JobModifyIndex": 11232 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/job_job8_UDP.json b/pkg/provider/nomad/fixtures/job_job8_UDP.json new file mode 100644 index 000000000..eda3df6f0 --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job8_UDP.json @@ -0,0 +1,221 @@ +{ + "Stop": false, + "Region": "global", + "Namespace": "default", + "ID": "job8", + "ParentID": "", + "Name": "job8", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 1, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": { + "ID": "3828aaa9-4e16-cde5-cc80-f0cec4a7c82d", + "Type": "horizontal", + "Target": { + "Namespace": "default", + "Job": "job8", + "Group": "group1" + }, + "Policy": null, + "Min": 0, + "Max": 3, + "Enabled": true, + "CreateIndex": 0, + "ModifyIndex": 0 + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "ports": [ + "http" + ], + "image": "nginx" + }, + "Env": null, + "Services": null, + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": [ + { + "Name": "job8", + "TaskName": "", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true", + "traefik.udp.routers.job8.service=job8" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "running", + "StatusDescription": "", + "Stable": true, + "Version": 2, + "SubmitTime": 1705753285493029600, + "CreateIndex": 11276, + "ModifyIndex": 11303, + "JobModifyIndex": 11297 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/job_job9_ScalingEnabled_Stopped.json b/pkg/provider/nomad/fixtures/job_job9_ScalingEnabled_Stopped.json new file mode 100644 index 000000000..596072a67 --- /dev/null +++ b/pkg/provider/nomad/fixtures/job_job9_ScalingEnabled_Stopped.json @@ -0,0 +1,220 @@ +{ + "Stop": true, + "Region": "global", + "Namespace": "default", + "ID": "job9", + "ParentID": "", + "Name": "job9", + "Type": "service", + "Priority": 50, + "AllAtOnce": false, + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Constraints": null, + "Affinities": null, + "Spreads": null, + "TaskGroups": [ + { + "Name": "group1", + "Count": 1, + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000, + "ProgressDeadline": 600000000000, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Migrate": { + "MaxParallel": 1, + "HealthCheck": "checks", + "MinHealthyTime": 10000000000, + "HealthyDeadline": 300000000000 + }, + "Constraints": [ + { + "LTarget": "${attr.nomad.service_discovery}", + "RTarget": "true", + "Operand": "=" + } + ], + "Scaling": { + "ID": "094d38a7-24cf-d3a4-3a82-cfc50ef7c597", + "Type": "horizontal", + "Target": { + "Group": "group1", + "Namespace": "default", + "Job": "job9" + }, + "Policy": null, + "Min": 0, + "Max": 3, + "Enabled": true, + "CreateIndex": 0, + "ModifyIndex": 0 + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "Tasks": [ + { + "Name": "task1", + "Driver": "docker", + "User": "", + "Config": { + "image": "nginx", + "ports": [ + "http" + ] + }, + "Env": null, + "Services": null, + "Vault": null, + "Templates": null, + "Constraints": null, + "Affinities": null, + "Resources": { + "CPU": 100, + "Cores": 0, + "MemoryMB": 300, + "MemoryMaxMB": 0, + "DiskMB": 0, + "IOPS": 0, + "Networks": null, + "Devices": null + }, + "RestartPolicy": { + "Attempts": 2, + "Interval": 1800000000000, + "Delay": 15000000000, + "Mode": "fail", + "RenderTemplates": false + }, + "DispatchPayload": null, + "Lifecycle": null, + "Meta": null, + "KillTimeout": 5000000000, + "LogConfig": { + "MaxFiles": 10, + "MaxFileSizeMB": 10, + "Disabled": false + }, + "Artifacts": null, + "Leader": false, + "ShutdownDelay": 0, + "VolumeMounts": null, + "ScalingPolicies": null, + "KillSignal": "", + "Kind": "", + "CSIPluginConfig": null, + "Identity": null + } + ], + "EphemeralDisk": { + "Sticky": false, + "SizeMB": 300, + "Migrate": false + }, + "Meta": null, + "ReschedulePolicy": { + "Attempts": 0, + "Interval": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "Affinities": null, + "Spreads": null, + "Networks": [ + { + "Mode": "", + "Device": "", + "CIDR": "", + "IP": "", + "Hostname": "", + "MBits": 0, + "DNS": null, + "ReservedPorts": null, + "DynamicPorts": [ + { + "Label": "http", + "Value": 0, + "To": 80, + "HostNetwork": "default" + } + ] + } + ], + "Consul": { + "Namespace": "" + }, + "Services": [ + { + "Name": "job9", + "TaskName": "", + "PortLabel": "http", + "AddressMode": "auto", + "Address": "", + "EnableTagOverride": false, + "Tags": [ + "traefik.enable=true" + ], + "CanaryTags": null, + "Checks": null, + "Connect": null, + "Meta": null, + "CanaryMeta": null, + "TaggedAddresses": null, + "Namespace": "default", + "OnUpdate": "require_healthy", + "Provider": "nomad" + } + ], + "Volumes": null, + "ShutdownDelay": null, + "StopAfterClientDisconnect": null, + "MaxClientDisconnect": null + } + ], + "Update": { + "Stagger": 30000000000, + "MaxParallel": 1, + "HealthCheck": "", + "MinHealthyTime": 0, + "HealthyDeadline": 0, + "ProgressDeadline": 0, + "AutoRevert": false, + "AutoPromote": false, + "Canary": 0 + }, + "Multiregion": null, + "Periodic": null, + "ParameterizedJob": null, + "Dispatched": false, + "DispatchIdempotencyToken": "", + "Payload": null, + "Meta": null, + "ConsulToken": "", + "ConsulNamespace": "", + "VaultToken": "", + "VaultNamespace": "", + "NomadTokenID": "", + "Status": "dead", + "StatusDescription": "", + "Stable": true, + "Version": 3, + "SubmitTime": 1705753979676559600, + "CreateIndex": 11317, + "ModifyIndex": 11346, + "JobModifyIndex": 11344 +} \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job1.json b/pkg/provider/nomad/fixtures/jobs_job1.json new file mode 100644 index 000000000..66016a6a9 --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job1.json @@ -0,0 +1,56 @@ +[ + { + "ID": "job1", + "ParentID": "", + "Name": "job1", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": false, + "Status": "running", + "StatusDescription": "", + "JobSummary": { + "JobID": "job1", + "Namespace": "default", + "Summary": { + "job1": { + "Queued": 0, + "Complete": 1, + "Failed": 0, + "Running": 0, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + }, + "group1": { + "Queued": 0, + "Complete": 6, + "Failed": 1, + "Running": 1, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 493, + "ModifyIndex": 9909 + }, + "CreateIndex": 493, + "ModifyIndex": 9961, + "JobModifyIndex": 9955, + "SubmitTime": 1705690395733241600, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job2.json b/pkg/provider/nomad/fixtures/jobs_job2.json new file mode 100644 index 000000000..ee817aa0a --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job2.json @@ -0,0 +1,47 @@ +[ + { + "ID": "job2", + "ParentID": "", + "Name": "job2", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": false, + "Status": "dead", + "StatusDescription": "", + "JobSummary": { + "JobID": "job2", + "Namespace": "default", + "Summary": { + "group1": { + "Queued": 0, + "Complete": 6, + "Failed": 6, + "Running": 0, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 2923, + "ModifyIndex": 10051 + }, + "CreateIndex": 2923, + "ModifyIndex": 10048, + "JobModifyIndex": 10044, + "SubmitTime": 1705690880440177400, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job3.json b/pkg/provider/nomad/fixtures/jobs_job3.json new file mode 100644 index 000000000..bf69bc069 --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job3.json @@ -0,0 +1,47 @@ +[ + { + "ID": "job3", + "ParentID": "", + "Name": "job3", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": false, + "Status": "running", + "StatusDescription": "", + "JobSummary": { + "JobID": "job3", + "Namespace": "default", + "Summary": { + "group1": { + "Queued": 0, + "Complete": 1, + "Failed": 0, + "Running": 1, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 2932, + "ModifyIndex": 9989 + }, + "CreateIndex": 2932, + "ModifyIndex": 9992, + "JobModifyIndex": 9983, + "SubmitTime": 1705690892484556300, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job4.json b/pkg/provider/nomad/fixtures/jobs_job4.json new file mode 100644 index 000000000..dfd0f3a11 --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job4.json @@ -0,0 +1,47 @@ +[ + { + "ID": "job4", + "ParentID": "", + "Name": "job4", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": true, + "Status": "dead", + "StatusDescription": "", + "JobSummary": { + "JobID": "job4", + "Namespace": "default", + "Summary": { + "group1": { + "Queued": 0, + "Complete": 6, + "Failed": 0, + "Running": 0, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 3079, + "ModifyIndex": 10057 + }, + "CreateIndex": 3079, + "ModifyIndex": 10054, + "JobModifyIndex": 10052, + "SubmitTime": 1705690905317339000, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job5.json b/pkg/provider/nomad/fixtures/jobs_job5.json new file mode 100644 index 000000000..bf846ed21 --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job5.json @@ -0,0 +1,56 @@ +[ + { + "ID": "job5", + "ParentID": "", + "Name": "job5", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": false, + "Status": "running", + "StatusDescription": "", + "JobSummary": { + "JobID": "job5", + "Namespace": "default", + "Summary": { + "job5": { + "Queued": 0, + "Complete": 2, + "Failed": 0, + "Running": 0, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + }, + "group1": { + "Queued": 0, + "Complete": 4, + "Failed": 0, + "Running": 1, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 3095, + "ModifyIndex": 10015 + }, + "CreateIndex": 3095, + "ModifyIndex": 10018, + "JobModifyIndex": 10008, + "SubmitTime": 1705690918813110300, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job6.json b/pkg/provider/nomad/fixtures/jobs_job6.json new file mode 100644 index 000000000..e4c34e238 --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job6.json @@ -0,0 +1,47 @@ +[ + { + "ID": "job6", + "ParentID": "", + "Name": "job6", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": false, + "Status": "dead", + "StatusDescription": "", + "JobSummary": { + "JobID": "job6", + "Namespace": "default", + "Summary": { + "group1": { + "Queued": 0, + "Complete": 3, + "Failed": 0, + "Running": 0, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 9941, + "ModifyIndex": 10082 + }, + "CreateIndex": 9941, + "ModifyIndex": 10078, + "JobModifyIndex": 10074, + "SubmitTime": 1705690931854150700, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job7.json b/pkg/provider/nomad/fixtures/jobs_job7.json new file mode 100644 index 000000000..f3de421e2 --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job7.json @@ -0,0 +1,47 @@ +[ + { + "ID": "job7", + "ParentID": "", + "Name": "job7", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": false, + "Status": "running", + "StatusDescription": "", + "JobSummary": { + "JobID": "job7", + "Namespace": "default", + "Summary": { + "group1": { + "Queued": 0, + "Complete": 0, + "Failed": 0, + "Running": 1, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 11221, + "ModifyIndex": 11225 + }, + "CreateIndex": 11221, + "ModifyIndex": 11238, + "JobModifyIndex": 11232, + "SubmitTime": 1705752438438572000, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job8.json b/pkg/provider/nomad/fixtures/jobs_job8.json new file mode 100644 index 000000000..e77e2c718 --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job8.json @@ -0,0 +1,47 @@ +[ + { + "ID": "job8", + "ParentID": "", + "Name": "job8", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": false, + "Status": "running", + "StatusDescription": "", + "JobSummary": { + "JobID": "job8", + "Namespace": "default", + "Summary": { + "group1": { + "Queued": 0, + "Complete": 0, + "Failed": 0, + "Running": 1, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 11276, + "ModifyIndex": 11281 + }, + "CreateIndex": 11276, + "ModifyIndex": 11303, + "JobModifyIndex": 11297, + "SubmitTime": 1705753285493029600, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/jobs_job9.json b/pkg/provider/nomad/fixtures/jobs_job9.json new file mode 100644 index 000000000..2f45e4ae7 --- /dev/null +++ b/pkg/provider/nomad/fixtures/jobs_job9.json @@ -0,0 +1,47 @@ +[ + { + "ID": "job9", + "ParentID": "", + "Name": "job9", + "Namespace": "default", + "Datacenters": [ + "dc1" + ], + "NodePool": "default", + "Multiregion": null, + "Type": "service", + "Priority": 50, + "Periodic": false, + "ParameterizedJob": false, + "Stop": true, + "Status": "dead", + "StatusDescription": "", + "JobSummary": { + "JobID": "job9", + "Namespace": "default", + "Summary": { + "group1": { + "Queued": 0, + "Complete": 1, + "Failed": 0, + "Running": 0, + "Starting": 0, + "Lost": 0, + "Unknown": 0 + } + }, + "Children": { + "Pending": 0, + "Running": 0, + "Dead": 0 + }, + "CreateIndex": 11317, + "ModifyIndex": 11349 + }, + "CreateIndex": 11317, + "ModifyIndex": 11346, + "JobModifyIndex": 11344, + "SubmitTime": 1705753979676559600, + "Meta": null + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_hello.json b/pkg/provider/nomad/fixtures/service_hello.json new file mode 100644 index 000000000..e4f675b16 --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_hello.json @@ -0,0 +1,20 @@ +[ + { + "Address": "127.0.0.1", + "AllocID": "71a63a80-a98a-93ee-4fd7-73b808577c20", + "CreateIndex": 18, + "Datacenter": "dc1", + "ID": "_nomad-task-71a63a80-a98a-93ee-4fd7-73b808577c20-group-hello-nomad-hello-nomad-http", + "JobID": "echo", + "ModifyIndex": 18, + "Namespace": "default", + "NodeID": "6d7f412e-e7ff-2e66-d47b-867b0e9d8726", + "Port": 20627, + "ServiceName": "hello-nomad", + "Tags": [ + "traefik.enable=true", + "traefik.http.routers.hellon.entrypoints=web", + "traefik.http.routers.hellon.service=hello-nomad" + ] + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job1.json b/pkg/provider/nomad/fixtures/service_job1.json new file mode 100644 index 000000000..18a6c2ba0 --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job1.json @@ -0,0 +1,18 @@ +[ + { + "ID": "_nomad-task-03c7270c-f475-5981-1932-87c0a8a5aa24-group-group1-job1-http", + "ServiceName": "job1", + "Namespace": "default", + "NodeID": "e262ecb6-a7ac-7c3e-4de0-06445559e596", + "Datacenter": "dc1", + "JobID": "job1", + "AllocID": "03c7270c-f475-5981-1932-87c0a8a5aa24", + "Tags": [ + "traefik.enable=true" + ], + "Address": "192.168.1.21", + "Port": 29916, + "CreateIndex": 5753, + "ModifyIndex": 9958 + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job2.json b/pkg/provider/nomad/fixtures/service_job2.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job2.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job3.json b/pkg/provider/nomad/fixtures/service_job3.json new file mode 100644 index 000000000..ece0f4235 --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job3.json @@ -0,0 +1,18 @@ +[ + { + "ID": "_nomad-task-dd945b55-70fa-0efc-6512-b88fb55ce33f-group-group1-job3-http", + "ServiceName": "job3", + "Namespace": "default", + "NodeID": "e262ecb6-a7ac-7c3e-4de0-06445559e596", + "Datacenter": "dc1", + "JobID": "job3", + "AllocID": "dd945b55-70fa-0efc-6512-b88fb55ce33f", + "Tags": [ + "traefik.enable=true" + ], + "Address": "192.168.1.21", + "Port": 29983, + "CreateIndex": 9987, + "ModifyIndex": 9987 + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job4.json b/pkg/provider/nomad/fixtures/service_job4.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job4.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job5task1.json b/pkg/provider/nomad/fixtures/service_job5task1.json new file mode 100644 index 000000000..a438e607f --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job5task1.json @@ -0,0 +1,18 @@ +[ + { + "ID": "_nomad-task-a98bac3d-5382-3032-1954-57aff58b20c1-task1-job5task1-http", + "ServiceName": "job5task1", + "Namespace": "default", + "NodeID": "e262ecb6-a7ac-7c3e-4de0-06445559e596", + "Datacenter": "dc1", + "JobID": "job5", + "AllocID": "a98bac3d-5382-3032-1954-57aff58b20c1", + "Tags": [ + "traefik.enable=true" + ], + "Address": "192.168.1.21", + "Port": 24542, + "CreateIndex": 10013, + "ModifyIndex": 10013 + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job5task2.json b/pkg/provider/nomad/fixtures/service_job5task2.json new file mode 100644 index 000000000..a9d32113f --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job5task2.json @@ -0,0 +1,18 @@ +[ + { + "ID": "_nomad-task-a98bac3d-5382-3032-1954-57aff58b20c1-task2-job5task2-other", + "ServiceName": "job5task2", + "Namespace": "default", + "NodeID": "e262ecb6-a7ac-7c3e-4de0-06445559e596", + "Datacenter": "dc1", + "JobID": "job5", + "AllocID": "a98bac3d-5382-3032-1954-57aff58b20c1", + "Tags": [ + "traefik.enable=true" + ], + "Address": "192.168.1.21", + "Port": 30165, + "CreateIndex": 10014, + "ModifyIndex": 10014 + } +] diff --git a/pkg/provider/nomad/fixtures/service_job6task1.json b/pkg/provider/nomad/fixtures/service_job6task1.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job6task1.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job6task2.json b/pkg/provider/nomad/fixtures/service_job6task2.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job6task2.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job7.json b/pkg/provider/nomad/fixtures/service_job7.json new file mode 100644 index 000000000..35ab344cf --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job7.json @@ -0,0 +1,20 @@ +[ + { + "ID": "_nomad-task-3e1cc853-f6b1-2c46-6f20-332a6e91794b-group-group1-job7-http", + "ServiceName": "job7", + "Namespace": "default", + "NodeID": "e262ecb6-a7ac-7c3e-4de0-06445559e596", + "Datacenter": "dc1", + "JobID": "job7", + "AllocID": "3e1cc853-f6b1-2c46-6f20-332a6e91794b", + "Tags": [ + "traefik.enable=true", + "traefik.tcp.routers.job7.rule=HostSNI(`job7`)", + "traefik.tcp.routers.job7.tls.passthrough=true" + ], + "Address": "192.168.1.21", + "Port": 22899, + "CreateIndex": 11224, + "ModifyIndex": 11271 + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job8.json b/pkg/provider/nomad/fixtures/service_job8.json new file mode 100644 index 000000000..d670c568a --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job8.json @@ -0,0 +1,19 @@ +[ + { + "ID": "_nomad-task-fffd3d81-66ac-ed6d-d9a1-fc41b2a26d07-group-group1-job8-http", + "ServiceName": "job8", + "Namespace": "default", + "NodeID": "e262ecb6-a7ac-7c3e-4de0-06445559e596", + "Datacenter": "dc1", + "JobID": "job8", + "AllocID": "fffd3d81-66ac-ed6d-d9a1-fc41b2a26d07", + "Tags": [ + "traefik.enable=true", + "traefik.udp.routers.job8.service=job8" + ], + "Address": "192.168.1.21", + "Port": 24268, + "CreateIndex": 11279, + "ModifyIndex": 11300 + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_job9.json b/pkg/provider/nomad/fixtures/service_job9.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_job9.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/service_redis.json b/pkg/provider/nomad/fixtures/service_redis.json new file mode 100644 index 000000000..b1f4e752e --- /dev/null +++ b/pkg/provider/nomad/fixtures/service_redis.json @@ -0,0 +1,18 @@ +[ + { + "Address": "127.0.0.1", + "AllocID": "07501480-8175-8071-7da6-133bd1ff890f", + "CreateIndex": 46, + "Datacenter": "dc1", + "ID": "_nomad-task-07501480-8175-8071-7da6-133bd1ff890f-group-redis-redis-redis", + "JobID": "echo", + "ModifyIndex": 46, + "Namespace": "default", + "NodeID": "6d7f412e-e7ff-2e66-d47b-867b0e9d8726", + "Port": 30826, + "ServiceName": "redis", + "Tags": [ + "traefik.enable=true" + ] + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/fixtures/services.json b/pkg/provider/nomad/fixtures/services.json new file mode 100644 index 000000000..dfe303183 --- /dev/null +++ b/pkg/provider/nomad/fixtures/services.json @@ -0,0 +1,21 @@ +[ + { + "Namespace": "default", + "Services": [ + { + "ServiceName": "redis", + "Tags": [ + "traefik.enable=true" + ] + }, + { + "ServiceName": "hello-nomad", + "Tags": [ + "traefik.enable=true", + "traefik.http.routers.hellon.entrypoints=web", + "traefik.http.routers.hellon.service=hello-nomad" + ] + } + ] + } +] \ No newline at end of file diff --git a/pkg/provider/nomad/nomad.go b/pkg/provider/nomad/nomad.go index b836707eb..5a595dca6 100644 --- a/pkg/provider/nomad/nomad.go +++ b/pkg/provider/nomad/nomad.go @@ -85,13 +85,14 @@ func (p *ProviderBuilder) BuildProviders() []*Provider { // Configuration represents the Nomad provider configuration. type Configuration struct { - DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` - Constraints string `description:"Constraints is an expression that Traefik matches against the Nomad service's tags to determine whether to create route(s) for that service." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` - Endpoint *EndpointConfig `description:"Nomad endpoint settings" json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" export:"true"` - Prefix string `description:"Prefix for nomad service tags." json:"prefix,omitempty" toml:"prefix,omitempty" yaml:"prefix,omitempty" export:"true"` - Stale bool `description:"Use stale consistency for catalog reads." json:"stale,omitempty" toml:"stale,omitempty" yaml:"stale,omitempty" export:"true"` - ExposedByDefault bool `description:"Expose Nomad services by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"` - RefreshInterval ptypes.Duration `description:"Interval for polling Nomad API." json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"` + DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` + Constraints string `description:"Constraints is an expression that Traefik matches against the Nomad service's tags to determine whether to create route(s) for that service." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` + Endpoint *EndpointConfig `description:"Nomad endpoint settings" json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" export:"true"` + Prefix string `description:"Prefix for nomad service tags." json:"prefix,omitempty" toml:"prefix,omitempty" yaml:"prefix,omitempty" export:"true"` + Stale bool `description:"Use stale consistency for catalog reads." json:"stale,omitempty" toml:"stale,omitempty" yaml:"stale,omitempty" export:"true"` + ExposedByDefault bool `description:"Expose Nomad services by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"` + RefreshInterval ptypes.Duration `description:"Interval for polling Nomad API." json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"` + AllowEmptyServices bool `description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` } // SetDefaults sets the default values for the Nomad Traefik Provider Configuration. @@ -116,6 +117,7 @@ func (c *Configuration) SetDefaults() { c.ExposedByDefault = true c.RefreshInterval = ptypes.Duration(15 * time.Second) c.DefaultRule = defaultTemplateRule + c.AllowEmptyServices = false } type EndpointConfig struct { @@ -222,10 +224,20 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. } func (p *Provider) loadConfiguration(ctx context.Context, configurationC chan<- dynamic.Message) error { - items, err := p.getNomadServiceData(ctx) - if err != nil { - return err + var items []item + var err error + if p.AllowEmptyServices { + items, err = p.getNomadServiceDataWithEmptyServices(ctx) + if err != nil { + return err + } + } else { + items, err = p.getNomadServiceData(ctx) + if err != nil { + return err + } } + configurationC <- dynamic.Message{ ProviderName: p.name, Configuration: p.buildConfig(ctx, items), @@ -291,6 +303,98 @@ func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) { return items, nil } +func (p *Provider) getNomadServiceDataWithEmptyServices(ctx context.Context) ([]item, error) { + jobsOpts := &api.QueryOptions{AllowStale: p.Stale} + jobsOpts = jobsOpts.WithContext(ctx) + + jobStubs, _, err := p.client.Jobs().List(jobsOpts) + if err != nil { + return nil, err + } + + var items []item + + // Get Services even when they are scaled down to zero. Currently the nomad service interface does not support this. https://github.com/hashicorp/nomad/issues/19731 + for _, jobStub := range jobStubs { + jobInfoOpts := &api.QueryOptions{} + jobInfoOpts = jobInfoOpts.WithContext(ctx) + + job, _, err := p.client.Jobs().Info(jobStub.ID, jobInfoOpts) + if err != nil { + return nil, err + } + + for _, taskGroup := range job.TaskGroups { + services := []*api.Service{} + // Get all services in job -> taskgroup + services = append(services, taskGroup.Services...) + + // Get all services in job -> taskgroup -> tasks + for _, task := range taskGroup.Tasks { + services = append(services, task.Services...) + } + + for _, service := range services { + logger := log.Ctx(ctx).With().Str("serviceName", service.TaskName).Logger() + + extraConf := p.getExtraConf(service.Tags) + if !extraConf.Enable { + logger.Debug().Msg("Filter Nomad service that is not enabled") + continue + } + + matches, err := constraints.MatchTags(service.Tags, p.Constraints) + if err != nil { + logger.Error().Err(err).Msg("Error matching constraint expressions") + continue + } + + if !matches { + logger.Debug().Msgf("Filter Nomad service not matching constraints: %q", p.Constraints) + continue + } + + if nil != taskGroup.Scaling && *taskGroup.Scaling.Enabled && *taskGroup.Count == 0 { + // Add items without address + items = append(items, item{ + // Create a unique id for non registered services + ID: fmt.Sprintf("%s-%s-%s-%s-%s", *job.Namespace, *job.Name, *taskGroup.Name, service.TaskName, service.Name), + Name: service.Name, + Namespace: *job.Namespace, + Node: "", + Datacenter: "", + Address: "", + Port: -1, + Tags: service.Tags, + ExtraConf: p.getExtraConf(service.Tags), + }) + } else { + instances, err := p.fetchService(ctx, service.Name) + if err != nil { + return nil, err + } + + for _, i := range instances { + items = append(items, item{ + ID: i.ID, + Name: i.ServiceName, + Namespace: i.Namespace, + Node: i.NodeID, + Datacenter: i.Datacenter, + Address: i.Address, + Port: i.Port, + Tags: i.Tags, + ExtraConf: p.getExtraConf(i.Tags), + }) + } + } + } + } + } + + return items, nil +} + // getExtraConf returns a configuration with settings which are not part of the dynamic configuration (e.g. ".enable"). func (p *Provider) getExtraConf(tags []string) configuration { labels := tagsToLabels(tags, p.Prefix) diff --git a/pkg/provider/nomad/nomad_test.go b/pkg/provider/nomad/nomad_test.go index ffbb3f89d..2794fab0c 100644 --- a/pkg/provider/nomad/nomad_test.go +++ b/pkg/provider/nomad/nomad_test.go @@ -2,8 +2,11 @@ package nomad import ( "context" + "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" @@ -12,6 +15,17 @@ import ( "github.com/traefik/traefik/v3/pkg/types" ) +var responses = map[string][]byte{} + +func TestMain(m *testing.M) { + err := setup() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err.Error()) + os.Exit(1) + } + m.Run() +} + func Test_globalConfig(t *testing.T) { cases := []struct { Name string @@ -131,15 +145,311 @@ func TestProvider_SetDefaults_Endpoint(t *testing.T) { } } +func Test_getNomadServiceDataWithEmptyServices_GroupService_Scaling1(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job1"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job1"): + _, _ = w.Write(responses["job_job1_WithGroupService_Scaling1"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job1"): + _, _ = w.Write(responses["service_job1"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + require.Len(t, items, 1) +} + +func Test_getNomadServiceDataWithEmptyServices_GroupService_Scaling0(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job2"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job2"): + _, _ = w.Write(responses["job_job2_WithGroupService_Scaling0"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job2"): + _, _ = w.Write(responses["service_job2"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + require.Len(t, items, 1) +} + +func Test_getNomadServiceDataWithEmptyServices_GroupService_ScalingDisabled(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job3"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job3"): + _, _ = w.Write(responses["job_job3_WithGroupService_ScalingDisabled"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job3"): + _, _ = w.Write(responses["service_job3"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + require.Len(t, items, 1) +} + +func Test_getNomadServiceDataWithEmptyServices_GroupService_ScalingDisabled_Stopped(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job4"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job4"): + _, _ = w.Write(responses["job_job4_WithGroupService_ScalingDisabled_Stopped"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job4"): + _, _ = w.Write(responses["service_job4"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + + // Should not be listed as job is stopped + require.Empty(t, items) +} + +func Test_getNomadServiceDataWithEmptyServices_GroupTaskService_Scaling1(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job5"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job5"): + _, _ = w.Write(responses["job_job5_WithGroupTaskService_Scaling1"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job5task1"): + _, _ = w.Write(responses["service_job5task1"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job5task2"): + _, _ = w.Write(responses["service_job5task2"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + require.Len(t, items, 2) +} + +func Test_getNomadServiceDataWithEmptyServices_GroupTaskService_Scaling0(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job6"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job6"): + _, _ = w.Write(responses["job_job6_WithGroupTaskService_Scaling0"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job6task1"): + _, _ = w.Write(responses["service_job6task1"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job6task2"): + _, _ = w.Write(responses["service_job6task2"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + require.Len(t, items, 2) +} + +func Test_getNomadServiceDataWithEmptyServices_TCP(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job7"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job7"): + _, _ = w.Write(responses["job_job7_TCP"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job7"): + _, _ = w.Write(responses["service_job7"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + require.Len(t, items, 1) +} + +func Test_getNomadServiceDataWithEmptyServices_UDP(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job8"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job8"): + _, _ = w.Write(responses["job_job8_UDP"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job8"): + _, _ = w.Write(responses["service_job8"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + require.Len(t, items, 1) +} + +func Test_getNomadServiceDataWithEmptyServices_ScalingEnabled_Stopped(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.RequestURI, "/v1/jobs"): + _, _ = w.Write(responses["jobs_job9"]) + case strings.HasSuffix(r.RequestURI, "/v1/job/job9"): + _, _ = w.Write(responses["job_job9_ScalingEnabled_Stopped"]) + case strings.HasSuffix(r.RequestURI, "/v1/service/job9"): + _, _ = w.Write(responses["service_job9"]) + } + })) + + t.Cleanup(ts.Close) + + p := new(Provider) + p.SetDefaults() + p.Endpoint.Address = ts.URL + err := p.Init() + require.NoError(t, err) + + // fudge client, avoid starting up via Provide + p.client, err = createClient(p.namespace, p.Endpoint) + require.NoError(t, err) + + // make the query for services + items, err := p.getNomadServiceDataWithEmptyServices(context.TODO()) + require.NoError(t, err) + + // Should not be listed as job is stopped + require.Empty(t, items) +} + +func setup() error { + responsesDir := "./fixtures" + files, err := os.ReadDir(responsesDir) + if err != nil { + return err + } + for _, file := range files { + if !file.IsDir() && filepath.Ext(file.Name()) == ".json" { + content, err := os.ReadFile(filepath.Join(responsesDir, file.Name())) + if err != nil { + return err + } + responses[strings.TrimSuffix(filepath.Base(file.Name()), filepath.Ext(file.Name()))] = content + } + } + return nil +} + func Test_getNomadServiceData(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.RequestURI, "/v1/services"): - _, _ = w.Write([]byte(services)) + _, _ = w.Write(responses["services"]) case strings.HasSuffix(r.RequestURI, "/v1/service/redis"): - _, _ = w.Write([]byte(redis)) + _, _ = w.Write(responses["service_redis"]) case strings.HasSuffix(r.RequestURI, "/v1/service/hello-nomad"): - _, _ = w.Write([]byte(hello)) + _, _ = w.Write(responses["service_hello"]) } })) t.Cleanup(ts.Close) @@ -159,71 +469,3 @@ func Test_getNomadServiceData(t *testing.T) { require.NoError(t, err) require.Len(t, items, 2) } - -const services = ` -[ - { - "Namespace": "default", - "Services": [ - { - "ServiceName": "redis", - "Tags": [ - "traefik.enable=true" - ] - }, - { - "ServiceName": "hello-nomad", - "Tags": [ - "traefik.enable=true", - "traefik.http.routers.hellon.entrypoints=web", - "traefik.http.routers.hellon.service=hello-nomad" - ] - } - ] - } -] -` - -const redis = ` -[ - { - "Address": "127.0.0.1", - "AllocID": "07501480-8175-8071-7da6-133bd1ff890f", - "CreateIndex": 46, - "Datacenter": "dc1", - "ID": "_nomad-task-07501480-8175-8071-7da6-133bd1ff890f-group-redis-redis-redis", - "JobID": "echo", - "ModifyIndex": 46, - "Namespace": "default", - "NodeID": "6d7f412e-e7ff-2e66-d47b-867b0e9d8726", - "Port": 30826, - "ServiceName": "redis", - "Tags": [ - "traefik.enable=true" - ] - } -] -` - -const hello = ` -[ - { - "Address": "127.0.0.1", - "AllocID": "71a63a80-a98a-93ee-4fd7-73b808577c20", - "CreateIndex": 18, - "Datacenter": "dc1", - "ID": "_nomad-task-71a63a80-a98a-93ee-4fd7-73b808577c20-group-hello-nomad-hello-nomad-http", - "JobID": "echo", - "ModifyIndex": 18, - "Namespace": "default", - "NodeID": "6d7f412e-e7ff-2e66-d47b-867b0e9d8726", - "Port": 20627, - "ServiceName": "hello-nomad", - "Tags": [ - "traefik.enable=true", - "traefik.http.routers.hellon.entrypoints=web", - "traefik.http.routers.hellon.service=hello-nomad" - ] - } -] -` From 998c6174cd4172306816ef4c743d2daa51a33de0 Mon Sep 17 00:00:00 2001 From: Martijn Cremer Date: Fri, 5 Apr 2024 10:14:03 +0200 Subject: [PATCH 04/30] Improved documentation about Nomad ACL minimum rights --- docs/content/providers/nomad.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/providers/nomad.md b/docs/content/providers/nomad.md index 8ff435b2a..5c565b055 100644 --- a/docs/content/providers/nomad.md +++ b/docs/content/providers/nomad.md @@ -163,6 +163,7 @@ providers: _Optional, Default=""_ Token is used to provide a per-request ACL token, if Nomad ACLs are enabled. +The appropriate ACL privilege for this token is 'read-job', as outlined in the [Nomad documentation on ACL](https://developer.hashicorp.com/nomad/tutorials/access-control/access-control-policies). ```yaml tab="File (YAML)" providers: From e5062cef420e6c8ced63687a8a02962ec1577224 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:14:04 +0200 Subject: [PATCH 05/30] chore: update dependencies --- go.mod | 32 +++++++++++++-------------- go.sum | 70 ++++++++++++++++++++++++++++------------------------------ 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index c9cd7a9ab..4f8f1c373 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/docker/cli v24.0.7+incompatible - github.com/docker/docker v24.0.7+incompatible + github.com/docker/cli v24.0.9+incompatible + github.com/docker/docker v24.0.9+incompatible github.com/docker/go-connections v0.4.0 github.com/fatih/structs v1.1.0 github.com/fsnotify/fsnotify v1.7.0 @@ -51,7 +51,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_model v0.3.0 - github.com/quic-go/quic-go v0.40.1 + github.com/quic-go/quic-go v0.42.0 github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 @@ -67,11 +67,11 @@ require ( github.com/vulcand/predicate v1.2.0 go.elastic.co/apm/module/apmot/v2 v2.4.8 go.elastic.co/apm/v2 v2.4.8 - golang.org/x/mod v0.14.0 - golang.org/x/net v0.20.0 + golang.org/x/mod v0.17.0 + golang.org/x/net v0.24.0 golang.org/x/text v0.14.0 golang.org/x/time v0.5.0 - golang.org/x/tools v0.17.0 + golang.org/x/tools v0.20.0 google.golang.org/grpc v1.59.0 gopkg.in/DataDog/dd-trace-go.v1 v1.56.1 gopkg.in/yaml.v3 v3.0.1 @@ -170,7 +170,7 @@ require ( github.com/go-errors/errors v1.0.1 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect @@ -188,7 +188,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect + github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f // indirect github.com/google/s2a-go v0.1.5 // indirect github.com/google/uuid v1.4.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect @@ -256,7 +256,7 @@ require ( github.com/nrdcg/porkbun v0.3.0 // indirect github.com/nzdjb/go-metaname v1.0.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/onsi/ginkgo/v2 v2.17.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/opencontainers/runc v1.1.7 // indirect @@ -272,7 +272,6 @@ require ( github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/redis/go-redis/v9 v9.2.1 // indirect github.com/sacloud/api-client-go v0.2.8 // indirect github.com/sacloud/go-http v0.1.6 // indirect @@ -310,24 +309,25 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/goleak v1.3.0 // indirect - go.uber.org/mock v0.3.0 // indirect + go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.21.0 // indirect go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect - golang.org/x/crypto v0.19.0 // indirect - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.128.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect diff --git a/go.sum b/go.sum index cb7a4cfd6..d2a55c692 100644 --- a/go.sum +++ b/go.sum @@ -318,12 +318,12 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dnsimple/dnsimple-go v1.2.0 h1:ddTGyLVKly5HKb5L65AkLqFqwZlWo3WnR0BlFZlIddM= github.com/dnsimple/dnsimple-go v1.2.0/go.mod h1:z/cs26v/eiRvUyXsHQBLd8lWF8+cD6GbmkPH84plM4U= -github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= -github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50= +github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -430,8 +430,8 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -579,8 +579,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f h1:f00RU+zOX+B3rLAmMMkzHUF2h1z4DeYR9tTCvEq2REY= +github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.5 h1:8IYp3w9nysqv3JH+NJgXJzGbDHzLOTj43BmSkp+O7qg= github.com/google/s2a-go v0.1.5/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= @@ -980,8 +980,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= +github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -991,8 +991,8 @@ github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+t github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -1106,10 +1106,8 @@ github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= -github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q= -github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= +github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= +github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac h1:wBGhHdXKICZmvAPWS8gQoMyOWDH7QAi9bU4Z1nDWnFU= github.com/rancher/go-rancher-metadata v0.0.0-20200311180630-7f4c936a06ac/go.mod h1:67sLWL17mVlO1HFROaTBmU71NB4R8UNCesFHhg0f6LQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -1339,8 +1337,8 @@ go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= -go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -1389,8 +1387,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1404,8 +1402,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1436,8 +1434,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1498,8 +1496,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1527,8 +1525,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1632,8 +1630,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1644,8 +1642,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1746,8 +1744,8 @@ golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyj golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1889,8 +1887,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/DataDog/dd-trace-go.v1 v1.56.1 h1:AUe/ZF7xm6vYnigPe+TY54DmfWYJxhMRaw/TfvrbzvE= gopkg.in/DataDog/dd-trace-go.v1 v1.56.1/go.mod h1:KDLJ3CWVOSuVVwu+0ZR5KZo2rP6c7YyBV3v387dIpUU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= From f69fd43122ff1a77c989560bb2a63efe39202d22 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 5 Apr 2024 17:18:03 +0200 Subject: [PATCH 06/30] Add support for Kubernetes Gateway API RequestHeaderModifier filter Co-authored-by: Baptiste Mayelle --- .../routing/providers/kubernetes-gateway.md | 74 ++++++----- internal/gendoc.go | 8 +- pkg/config/dynamic/middlewares.go | 12 ++ pkg/config/dynamic/zz_generated.deepcopy.go | 40 ++++++ .../headermodifier/request_header_modifier.go | 56 ++++++++ .../request_header_modifier_test.go | 121 ++++++++++++++++++ .../filter_request_header_modifier.yml | 58 +++++++++ pkg/provider/kubernetes/gateway/kubernetes.go | 27 ++++ .../kubernetes/gateway/kubernetes_test.go | 69 ++++++++++ pkg/server/middleware/middlewares.go | 11 ++ .../components/_commons/PanelMiddlewares.vue | 55 ++++++++ 11 files changed, 499 insertions(+), 32 deletions(-) create mode 100644 pkg/middlewares/headermodifier/request_header_modifier.go create mode 100644 pkg/middlewares/headermodifier/request_header_modifier_test.go create mode 100644 pkg/provider/kubernetes/gateway/fixtures/httproute/filter_request_header_modifier.yml diff --git a/docs/content/routing/providers/kubernetes-gateway.md b/docs/content/routing/providers/kubernetes-gateway.md index 4649cf5ca..a40e18b63 100644 --- a/docs/content/routing/providers/kubernetes-gateway.md +++ b/docs/content/routing/providers/kubernetes-gateway.md @@ -251,39 +251,51 @@ Kubernetes cluster before creating `HTTPRoute` objects. requestRedirect: # [27] scheme: https # [28] statusCode: 301 # [29] + - type: RequestHeaderModifier # [30] + requestHeaderModifier: # [31] + set: + - name: X-Foo + value: Bar + add: + - name: X-Bar + value: Foo + remove: + - X-Baz ``` -| Ref | Attribute | Description | -|------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [1] | `parentRefs` | References the resources (usually Gateways) that a Route wants to be attached to. | -| [2] | `name` | Name of the referent. | -| [3] | `namespace` | Namespace of the referent. When unspecified (or empty string), this refers to the local namespace of the Route. | -| [4] | `sectionName` | Name of a section within the target resource (the Listener name). | -| [5] | `hostnames` | A set of hostname that should match against the HTTP Host header to select a HTTPRoute to process the request. | -| [6] | `rules` | A list of HTTP matchers, filters and actions. | -| [7] | `matches` | Conditions used for matching the rule against incoming HTTP requests. Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied. | -| [8] | `path` | An HTTP request path matcher. If this field is not specified, a default prefix match on the "/" path is provided. | -| [9] | `type` | Type of match against the path Value (supported types: `Exact`, `Prefix`). | -| [10] | `value` | The value of the HTTP path to match against. | -| [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. | -| [12] | `name` | Name of the HTTP header to be matched. | -| [13] | `value` | Value of HTTP Header to be matched. | -| [14] | `backendRefs` | Defines the backend(s) where matching requests should be sent. | -| [15] | `name` | The name of the referent service. | -| [16] | `weight` | The proportion of traffic forwarded to a targetRef, computed as weight/(sum of all weights in targetRefs). | -| [17] | `port` | The port of the referent service. | -| [18] | `group` | Group is the group of the referent. Only `traefik.io` and `gateway.networking.k8s.io` values are supported. | -| [19] | `kind` | Kind is kind of the referent. Only `TraefikService` and `Service` values are supported. | -| [20] | `filters` | Defines the filters (middlewares) applied to the route. | -| [21] | `type` | Defines the type of filter; ExtensionRef is used for configuring custom HTTP filters. | -| [22] | `extensionRef` | Configuration of the custom HTTP filter. | -| [23] | `group` | Group of the kubernetes object to reference. | -| [24] | `kind` | Kind of the kubernetes object to reference. | -| [25] | `name` | Name of the kubernetes object to reference. | -| [26] | `type` | Defines the type of filter; RequestRedirect redirects a request to another location. | -| [27] | `requestRedirect` | Configuration of redirect filter. | -| [28] | `scheme` | Scheme is the scheme to be used in the value of the Location header in the response. | -| [29] | `statusCode` | StatusCode is the HTTP status code to be used in response. | +| Ref | Attribute | Description | +|------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [1] | `parentRefs` | References the resources (usually Gateways) that a Route wants to be attached to. | +| [2] | `name` | Name of the referent. | +| [3] | `namespace` | Namespace of the referent. When unspecified (or empty string), this refers to the local namespace of the Route. | +| [4] | `sectionName` | Name of a section within the target resource (the Listener name). | +| [5] | `hostnames` | A set of hostname that should match against the HTTP Host header to select a HTTPRoute to process the request. | +| [6] | `rules` | A list of HTTP matchers, filters and actions. | +| [7] | `matches` | Conditions used for matching the rule against incoming HTTP requests. Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied. | +| [8] | `path` | An HTTP request path matcher. If this field is not specified, a default prefix match on the "/" path is provided. | +| [9] | `type` | Type of match against the path Value (supported types: `Exact`, `Prefix`). | +| [10] | `value` | The value of the HTTP path to match against. | +| [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. | +| [12] | `name` | Name of the HTTP header to be matched. | +| [13] | `value` | Value of HTTP Header to be matched. | +| [14] | `backendRefs` | Defines the backend(s) where matching requests should be sent. | +| [15] | `name` | The name of the referent service. | +| [16] | `weight` | The proportion of traffic forwarded to a targetRef, computed as weight/(sum of all weights in targetRefs). | +| [17] | `port` | The port of the referent service. | +| [18] | `group` | Group is the group of the referent. Only `traefik.io` and `gateway.networking.k8s.io` values are supported. | +| [19] | `kind` | Kind is kind of the referent. Only `TraefikService` and `Service` values are supported. | +| [20] | `filters` | Defines the filters (middlewares) applied to the route. | +| [21] | `type` | Defines the type of filter; ExtensionRef is used for configuring custom HTTP filters. | +| [22] | `extensionRef` | Configuration of the custom HTTP filter. | +| [23] | `group` | Group of the kubernetes object to reference. | +| [24] | `kind` | Kind of the kubernetes object to reference. | +| [25] | `name` | Name of the kubernetes object to reference. | +| [26] | `type` | Defines the type of filter; RequestRedirect redirects a request to another location. | +| [27] | `requestRedirect` | Configuration of redirect filter. | +| [28] | `scheme` | Scheme is the scheme to be used in the value of the Location header in the response. | +| [29] | `statusCode` | StatusCode is the HTTP status code to be used in response. | +| [30] | `type` | Defines the type of filter; RequestHeaderModifier modifies request headers. | +| [31] | `requestHeaderModifier` | Configuration of RequestHeaderModifier filter. | ### Kind: `TCPRoute` diff --git a/internal/gendoc.go b/internal/gendoc.go index deb7757ee..67eb64003 100644 --- a/internal/gendoc.go +++ b/internal/gendoc.go @@ -207,7 +207,13 @@ func clean(element any) { var svcFieldNames []string for i := range valueSvcRoot.NumField() { - svcFieldNames = append(svcFieldNames, valueSvcRoot.Type().Field(i).Name) + field := valueSvcRoot.Type().Field(i) + // do not create empty node for hidden config. + if field.Tag.Get("file") == "-" && field.Tag.Get("kv") == "-" && field.Tag.Get("label") == "-" { + continue + } + + svcFieldNames = append(svcFieldNames, field.Name) } sort.Strings(svcFieldNames) diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index a9be47306..6fbe2a0f5 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -39,6 +39,9 @@ type Middleware struct { GrpcWeb *GrpcWeb `json:"grpcWeb,omitempty" toml:"grpcWeb,omitempty" yaml:"grpcWeb,omitempty" export:"true"` Plugin map[string]PluginConf `json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty" export:"true"` + + // Gateway API HTTPRoute filters middlewares. + RequestHeaderModifier *RequestHeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` } // +k8s:deepcopy-gen=true @@ -673,3 +676,12 @@ type TLSClientCertificateSubjectDNInfo struct { // Users holds a list of users. type Users []string + +// +k8s:deepcopy-gen=true + +// RequestHeaderModifier holds the request header modifier configuration. +type RequestHeaderModifier struct { + Set map[string]string `json:"set,omitempty"` + Add map[string]string `json:"add,omitempty"` + Remove []string `json:"remove,omitempty"` +} diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index f04de4275..63b159382 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -859,6 +859,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { (*out)[key] = *val.DeepCopy() } } + if in.RequestHeaderModifier != nil { + in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier + *out = new(RequestHeaderModifier) + (*in).DeepCopyInto(*out) + } return } @@ -1067,6 +1072,41 @@ func (in *ReplacePathRegex) DeepCopy() *ReplacePathRegex { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RequestHeaderModifier) DeepCopyInto(out *RequestHeaderModifier) { + *out = *in + if in.Set != nil { + in, out := &in.Set, &out.Set + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Add != nil { + in, out := &in.Add, &out.Add + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Remove != nil { + in, out := &in.Remove, &out.Remove + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestHeaderModifier. +func (in *RequestHeaderModifier) DeepCopy() *RequestHeaderModifier { + if in == nil { + return nil + } + out := new(RequestHeaderModifier) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) { *out = *in diff --git a/pkg/middlewares/headermodifier/request_header_modifier.go b/pkg/middlewares/headermodifier/request_header_modifier.go new file mode 100644 index 000000000..3b197c2d5 --- /dev/null +++ b/pkg/middlewares/headermodifier/request_header_modifier.go @@ -0,0 +1,56 @@ +package headermodifier + +import ( + "context" + "net/http" + + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/middlewares" + "go.opentelemetry.io/otel/trace" +) + +const typeName = "RequestHeaderModifier" + +// requestHeaderModifier is a middleware used to modify the headers of an HTTP request. +type requestHeaderModifier struct { + next http.Handler + name string + + set map[string]string + add map[string]string + remove []string +} + +// NewRequestHeaderModifier creates a new request header modifier middleware. +func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) (http.Handler, error) { + logger := middlewares.GetLogger(ctx, name, typeName) + logger.Debug().Msg("Creating middleware") + + return &requestHeaderModifier{ + next: next, + name: name, + set: config.Set, + add: config.Add, + remove: config.Remove, + }, nil +} + +func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) { + return r.name, typeName, trace.SpanKindUnspecified +} + +func (r *requestHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + for headerName, headerValue := range r.set { + req.Header.Set(headerName, headerValue) + } + + for headerName, headerValue := range r.add { + req.Header.Add(headerName, headerValue) + } + + for _, headerName := range r.remove { + req.Header.Del(headerName) + } + + r.next.ServeHTTP(rw, req) +} diff --git a/pkg/middlewares/headermodifier/request_header_modifier_test.go b/pkg/middlewares/headermodifier/request_header_modifier_test.go new file mode 100644 index 000000000..60c446ecc --- /dev/null +++ b/pkg/middlewares/headermodifier/request_header_modifier_test.go @@ -0,0 +1,121 @@ +package headermodifier + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/testhelpers" +) + +func TestRequestHeaderModifier(t *testing.T) { + testCases := []struct { + desc string + config dynamic.RequestHeaderModifier + requestHeaders http.Header + expectedHeaders http.Header + }{ + { + desc: "no config", + config: dynamic.RequestHeaderModifier{}, + expectedHeaders: map[string][]string{}, + }, + { + desc: "set header", + config: dynamic.RequestHeaderModifier{ + Set: map[string]string{"Foo": "Bar"}, + }, + expectedHeaders: map[string][]string{"Foo": {"Bar"}}, + }, + { + desc: "set header with existing headers", + config: dynamic.RequestHeaderModifier{ + Set: map[string]string{"Foo": "Bar"}, + }, + requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, + expectedHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}}, + }, + { + desc: "set multiple headers with existing headers", + config: dynamic.RequestHeaderModifier{ + Set: map[string]string{"Foo": "Bar", "Bar": "Foo"}, + }, + requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}}, + expectedHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}}, + }, + { + desc: "add header", + config: dynamic.RequestHeaderModifier{ + Add: map[string]string{"Foo": "Bar"}, + }, + expectedHeaders: map[string][]string{"Foo": {"Bar"}}, + }, + { + desc: "add header with existing headers", + config: dynamic.RequestHeaderModifier{ + Add: map[string]string{"Foo": "Bar"}, + }, + requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, + expectedHeaders: map[string][]string{"Foo": {"Baz", "Bar"}, "Bar": {"Foo"}}, + }, + { + desc: "add multiple headers with existing headers", + config: dynamic.RequestHeaderModifier{ + Add: map[string]string{"Foo": "Bar", "Bar": "Foo"}, + }, + requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}}, + expectedHeaders: map[string][]string{"Foo": {"Baz", "Bar"}, "Bar": {"Foobar", "Foo"}}, + }, + { + desc: "remove header", + config: dynamic.RequestHeaderModifier{ + Remove: []string{"Foo"}, + }, + expectedHeaders: map[string][]string{}, + }, + { + desc: "remove header with existing headers", + config: dynamic.RequestHeaderModifier{ + Remove: []string{"Foo"}, + }, + requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, + expectedHeaders: map[string][]string{"Bar": {"Foo"}}, + }, + { + desc: "remove multiple headers with existing headers", + config: dynamic.RequestHeaderModifier{ + Remove: []string{"Foo", "Bar"}, + }, + requestHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}, "Baz": {"Bar"}}, + expectedHeaders: map[string][]string{"Baz": {"Bar"}}, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var gotHeaders http.Header + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotHeaders = r.Header + }) + + handler, err := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier") + require.NoError(t, err) + + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) + if test.requestHeaders != nil { + req.Header = test.requestHeaders + } + + resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) + + assert.Equal(t, test.expectedHeaders, gotHeaders) + }) + } +} diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_request_header_modifier.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_request_header_modifier.yml new file mode 100644 index 000000000..d5cc3d0b6 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_request_header_modifier.yml @@ -0,0 +1,58 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + hostnames: + - "example.org" + rules: + - backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Foo + value: Bar + add: + - name: X-Bar + value: Foo + remove: + - X-Baz diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 01401d750..4c355c3a5 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -1921,6 +1921,11 @@ func (p *Provider) loadMiddlewares(listener gatev1.Listener, namespace string, p } middlewares[name] = middleware + + case gatev1.HTTPRouteFilterRequestHeaderModifier: + middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) + middlewares[middlewareName] = createRequestHeaderModifier(filter.RequestHeaderModifier) + default: // As per the spec: // https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional @@ -1950,6 +1955,28 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe return filterFunc(string(extensionRef.Name), namespace) } +// createRequestHeaderModifier does not enforce/check the configuration, +// as the spec indicates that either the webhook or CEL (since v1.0 GA Release) should enforce that. +func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middleware { + sets := map[string]string{} + for _, header := range filter.Set { + sets[string(header.Name)] = header.Value + } + + adds := map[string]string{} + for _, header := range filter.Add { + adds[string(header.Name)] = header.Value + } + + return &dynamic.Middleware{ + RequestHeaderModifier: &dynamic.RequestHeaderModifier{ + Set: sets, + Add: adds, + Remove: filter.Remove, + }, + } +} + func createRedirectRegexMiddleware(scheme string, filter *gatev1.HTTPRequestRedirectFilter) (*dynamic.Middleware, error) { // Use the HTTPRequestRedirectFilter scheme if defined. filterScheme := scheme diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index f3c6f8c00..64f7c7f96 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -1517,6 +1517,75 @@ func TestLoadHTTPRoutes(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "Simple HTTPRoute, request header modifier", + paths: []string{"services.yml", "httproute/filter_request_header_modifier.yml"}, + entryPoints: map[string]Entrypoint{"web": { + Address: ":80", + }}, + 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{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr", + Rule: "Host(`example.org`) && PathPrefix(`/`)", + RuleSyntax: "v3", + Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestheadermodifier-0"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestheadermodifier-0": { + RequestHeaderModifier: &dynamic.RequestHeaderModifier{ + Set: map[string]string{"X-Foo": "Bar"}, + Add: map[string]string{"X-Bar": "Foo"}, + Remove: []string{"X-Baz"}, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-80", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, { desc: "Simple HTTPRoute, redirect HTTP to HTTPS", paths: []string{"services.yml", "httproute/filter_http_to_https.yml"}, diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 0955fbcae..2217dc58a 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -21,6 +21,7 @@ import ( "github.com/traefik/traefik/v3/pkg/middlewares/contenttype" "github.com/traefik/traefik/v3/pkg/middlewares/customerrors" "github.com/traefik/traefik/v3/pkg/middlewares/grpcweb" + "github.com/traefik/traefik/v3/pkg/middlewares/headermodifier" "github.com/traefik/traefik/v3/pkg/middlewares/headers" "github.com/traefik/traefik/v3/pkg/middlewares/inflightreq" "github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist" @@ -384,6 +385,16 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( } } + // Gateway API HTTPRoute filters middlewares. + if config.RequestHeaderModifier != nil { + if middleware != nil { + return nil, badConf + } + middleware = func(next http.Handler) (http.Handler, error) { + return headermodifier.NewRequestHeaderModifier(ctx, next, *config.RequestHeaderModifier, middlewareName) + } + } + if middleware == nil { return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName) } diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index 4b07ad2d9..4b2c24678 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -1491,6 +1491,61 @@ + + + +
+
+
+ Set +
+ + {{ key }}: {{ val }} + +
+
+
+ + +
+
+
+ Add +
+ + {{ key }}: {{ val }} + +
+
+
+ + +
+
+
+ Remove +
+ + {{ name }} + +
+
+
From cef842245c032c063a463f2baaf7c92ecc5a8070 Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 8 Apr 2024 17:16:04 +0200 Subject: [PATCH 07/30] Introduce Lingering Timeout Co-authored-by: Baptiste Mayelle Co-authored-by: Kevin Pollet --- docs/content/migration/v2.md | 32 ++++++ .../reference/static-configuration/cli-ref.md | 18 +++- .../reference/static-configuration/env-ref.md | 18 +++- .../reference/static-configuration/file.toml | 6 ++ .../reference/static-configuration/file.yaml | 6 ++ docs/content/routing/entrypoints.md | 81 +++++++++++---- pkg/api/handler_entrypoint_test.go | 22 +++-- pkg/api/testdata/entrypoints.json | 16 +-- pkg/config/static/static_config.go | 99 ++++++++++++++++++- pkg/redactor/redactor_config_test.go | 13 ++- .../testdata/anonymized-static-config.json | 11 ++- pkg/server/router/tcp/router.go | 12 --- pkg/server/server_entrypoint_tcp.go | 82 +++++++++++---- pkg/server/server_entrypoint_tcp_test.go | 88 ++++++++++++++++- 14 files changed, 418 insertions(+), 86 deletions(-) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 0f3114fc3..0a80cf43d 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -577,3 +577,35 @@ the maximum user-defined router priority value is: - `(MaxInt32 - 1000)` for 32-bit platforms, - `(MaxInt64 - 1000)` for 64-bit platforms. + +### .Transport.RespondingTimeouts. + +Starting with `v2.11.1` the following timeout options are deprecated: + +- `.transport.respondingTimeouts.readTimeout` +- `.transport.respondingTimeouts.writeTimeout` +- `.transport.respondingTimeouts.idleTimeout` + +They have been replaced by: + +- `.transport.respondingTimeouts.http.readTimeout` +- `.transport.respondingTimeouts.http.writeTimeout` +- `.transport.respondingTimeouts.http.idleTimeout` + +### .Transport.RespondingTimeouts.TCP.LingeringTimeout + +Starting with `v2.11.1` a new `lingeringTimeout` entryPoints option has been introduced, with a default value of 2s. + +The lingering timeout defines the maximum duration between each TCP read operation on the connection. +As a layer 4 timeout, it applies during HTTP handling but respects the configured HTTP server `readTimeout`. + +This change avoids Traefik instances with the default configuration hanging while waiting for bytes to be read on the connection. + +We suggest to adapt this value accordingly to your situation. +The new default value is purposely narrowed and can close the connection too early. + +Increasing the `lingeringTimeout` value could be the solution notably if you are dealing with the following errors: + +- TCP: `Error while handling TCP connection: readfrom tcp X.X.X.X:X->X.X.X.X:X: read tcp X.X.X.X:X->X.X.X.X:X: i/o timeout` +- HTTP: `'499 Client Closed Request' caused by: context canceled` +- HTTP: `ReverseProxy read error during body copy: read tcp X.X.X.X:X->X.X.X.X:X: use of closed network connection` diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 60a8c6513..e3ae10e2a 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -183,15 +183,27 @@ Duration to give active requests a chance to finish before Traefik stops. (Defau `--entrypoints..transport.lifecycle.requestacceptgracetimeout`: Duration to keep accepting requests before Traefik initiates the graceful shutdown procedure. (Default: ```0```) -`--entrypoints..transport.respondingtimeouts.idletimeout`: +`--entrypoints..transport.respondingtimeouts.http.idletimeout`: IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set. (Default: ```180```) -`--entrypoints..transport.respondingtimeouts.readtimeout`: +`--entrypoints..transport.respondingtimeouts.http.readtimeout`: ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set. (Default: ```0```) -`--entrypoints..transport.respondingtimeouts.writetimeout`: +`--entrypoints..transport.respondingtimeouts.http.writetimeout`: WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set. (Default: ```0```) +`--entrypoints..transport.respondingtimeouts.idletimeout`: +(Deprecated) IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set. (Default: ```0```) + +`--entrypoints..transport.respondingtimeouts.readtimeout`: +(Deprecated) ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set. (Default: ```0```) + +`--entrypoints..transport.respondingtimeouts.tcp.lingeringtimeout`: +LingeringTimeout is the maximum duration between each TCP read operation on the connection. If zero, no timeout is set. (Default: ```2```) + +`--entrypoints..transport.respondingtimeouts.writetimeout`: +(Deprecated) WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set. (Default: ```0```) + `--entrypoints..udp.timeout`: Timeout defines how long to wait on an idle session before releasing the related resources. (Default: ```3```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 849601f63..9ae961694 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -183,15 +183,27 @@ Duration to give active requests a chance to finish before Traefik stops. (Defau `TRAEFIK_ENTRYPOINTS__TRANSPORT_LIFECYCLE_REQUESTACCEPTGRACETIMEOUT`: Duration to keep accepting requests before Traefik initiates the graceful shutdown procedure. (Default: ```0```) -`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_IDLETIMEOUT`: +`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_HTTP_IDLETIMEOUT`: IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set. (Default: ```180```) -`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_READTIMEOUT`: +`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_HTTP_READTIMEOUT`: ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set. (Default: ```0```) -`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_WRITETIMEOUT`: +`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_HTTP_WRITETIMEOUT`: WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set. (Default: ```0```) +`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_IDLETIMEOUT`: +(Deprecated) IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set. (Default: ```0```) + +`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_READTIMEOUT`: +(Deprecated) ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set. (Default: ```0```) + +`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_TCP_LINGERINGTIMEOUT`: +LingeringTimeout is the maximum duration between each TCP read operation on the connection. If zero, no timeout is set. (Default: ```2```) + +`TRAEFIK_ENTRYPOINTS__TRANSPORT_RESPONDINGTIMEOUTS_WRITETIMEOUT`: +(Deprecated) WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set. (Default: ```0```) + `TRAEFIK_ENTRYPOINTS__UDP_TIMEOUT`: Timeout defines how long to wait on an idle session before releasing the related resources. (Default: ```3```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 42c4e6fdf..bb402467c 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -26,6 +26,12 @@ readTimeout = "42s" writeTimeout = "42s" idleTimeout = "42s" + [entryPoints.EntryPoint0.transport.respondingTimeouts.http] + readTimeout = "42s" + writeTimeout = "42s" + idleTimeout = "42s" + [entryPoints.EntryPoint0.transport.respondingTimeouts.tcp] + lingeringTimeout = "42s" [entryPoints.EntryPoint0.proxyProtocol] insecure = true trustedIPs = ["foobar", "foobar"] diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index abb8e05d8..d5265aa1d 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -24,6 +24,12 @@ entryPoints: readTimeout: 42s writeTimeout: 42s idleTimeout: 42s + http: + readTimeout: 42s + writeTimeout: 42s + idleTimeout: 42s + tcp: + lingeringTimeout: 42s keepAliveMaxTime: 42s keepAliveMaxRequests: 42 proxyProtocol: diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 70ce11cf5..c6a79ba8e 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -397,10 +397,11 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward #### `respondingTimeouts` -`respondingTimeouts` are timeouts for incoming requests to the Traefik instance. -Setting them has no effect for UDP entryPoints. +##### `http` -??? info "`transport.respondingTimeouts.readTimeout`" +`respondingTimeouts.http` are timeouts for incoming requests to the Traefik instance. + +??? info "`transport.respondingTimeouts.http.readTimeout`" _Optional, Default=0s_ @@ -417,7 +418,8 @@ Setting them has no effect for UDP entryPoints. address: ":8888" transport: respondingTimeouts: - readTimeout: 42 + http: + readTimeout: 42 ``` ```toml tab="File (TOML)" @@ -425,18 +427,17 @@ Setting them has no effect for UDP entryPoints. [entryPoints] [entryPoints.name] address = ":8888" - [entryPoints.name.transport] - [entryPoints.name.transport.respondingTimeouts] - readTimeout = 42 + [entryPoints.name.transport.respondingTimeouts.http] + readTimeout = 42 ``` ```bash tab="CLI" ## Static configuration --entryPoints.name.address=:8888 - --entryPoints.name.transport.respondingTimeouts.readTimeout=42 + --entryPoints.name.transport.respondingTimeouts.http.readTimeout=42 ``` -??? info "`transport.respondingTimeouts.writeTimeout`" +??? info "`transport.respondingTimeouts.http.writeTimeout`" _Optional, Default=0s_ @@ -454,7 +455,8 @@ Setting them has no effect for UDP entryPoints. address: ":8888" transport: respondingTimeouts: - writeTimeout: 42 + http: + writeTimeout: 42 ``` ```toml tab="File (TOML)" @@ -462,18 +464,17 @@ Setting them has no effect for UDP entryPoints. [entryPoints] [entryPoints.name] address = ":8888" - [entryPoints.name.transport] - [entryPoints.name.transport.respondingTimeouts] - writeTimeout = 42 + [entryPoints.name.transport.respondingTimeouts.http] + writeTimeout = 42 ``` ```bash tab="CLI" ## Static configuration --entryPoints.name.address=:8888 - --entryPoints.name.transport.respondingTimeouts.writeTimeout=42 + --entryPoints.name.transport.respondingTimeouts.http.writeTimeout=42 ``` -??? info "`transport.respondingTimeouts.idleTimeout`" +??? info "`transport.respondingTimeouts.http.idleTimeout`" _Optional, Default=180s_ @@ -490,7 +491,8 @@ Setting them has no effect for UDP entryPoints. address: ":8888" transport: respondingTimeouts: - idleTimeout: 42 + http: + idleTimeout: 42 ``` ```toml tab="File (TOML)" @@ -498,15 +500,54 @@ Setting them has no effect for UDP entryPoints. [entryPoints] [entryPoints.name] address = ":8888" - [entryPoints.name.transport] - [entryPoints.name.transport.respondingTimeouts] - idleTimeout = 42 + [entryPoints.name.transport.respondingTimeouts.http] + idleTimeout = 42 ``` ```bash tab="CLI" ## Static configuration --entryPoints.name.address=:8888 - --entryPoints.name.transport.respondingTimeouts.idleTimeout=42 + --entryPoints.name.transport.respondingTimeouts.http.idleTimeout=42 + +##### `tcp` + +`respondingTimeouts.tcp` are timeouts for client connections to the Traefik instance. + +??? info "`transport.respondingTimeouts.tcp.lingeringTimeout`" + + _Optional, Default=2s_ + + `lingeringTimeout` is the maximum duration between each TCP read operation on the connection. + As a layer 4 timeout, it also applies during HTTP handling, but respect the configured HTTP server `readTimeout`. + + If zero, the lingering is disabled. + Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits). + If no units are provided, the value is parsed assuming seconds. + + ```yaml tab="File (YAML)" + ## Static configuration + entryPoints: + name: + address: ":8888" + transport: + respondingTimeouts: + tcp: + lingeringTimeout: 42 + ``` + + ```toml tab="File (TOML)" + ## Static configuration + [entryPoints] + [entryPoints.name] + address = ":8888" + [entryPoints.name.transport.respondingTimeouts.tcp] + lingeringTimeout = 42 + ``` + + ```bash tab="CLI" + ## Static configuration + --entryPoints.name.address=:8888 + --entryPoints.name.transport.respondingTimeouts.tcp.lingeringTimeout=42 ``` #### `lifeCycle` diff --git a/pkg/api/handler_entrypoint_test.go b/pkg/api/handler_entrypoint_test.go index d5d6a5d1f..79b92a65f 100644 --- a/pkg/api/handler_entrypoint_test.go +++ b/pkg/api/handler_entrypoint_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v2/pkg/config/runtime" "github.com/traefik/traefik/v2/pkg/config/static" ) @@ -55,9 +56,11 @@ func TestHandler_EntryPoints(t *testing.T) { GraceTimeOut: 2, }, RespondingTimeouts: &static.RespondingTimeouts{ - ReadTimeout: 3, - WriteTimeout: 4, - IdleTimeout: 5, + HTTP: &static.HTTPRespondingTimeouts{ + ReadTimeout: paerserDurationPtr(3), + WriteTimeout: paerserDurationPtr(4), + IdleTimeout: paerserDurationPtr(5), + }, }, }, ProxyProtocol: &static.ProxyProtocol{ @@ -77,9 +80,11 @@ func TestHandler_EntryPoints(t *testing.T) { GraceTimeOut: 20, }, RespondingTimeouts: &static.RespondingTimeouts{ - ReadTimeout: 30, - WriteTimeout: 40, - IdleTimeout: 50, + HTTP: &static.HTTPRespondingTimeouts{ + ReadTimeout: paerserDurationPtr(3), + WriteTimeout: paerserDurationPtr(4), + IdleTimeout: paerserDurationPtr(5), + }, }, }, ProxyProtocol: &static.ProxyProtocol{ @@ -263,3 +268,8 @@ func generateEntryPoints(nb int) map[string]*static.EntryPoint { return eps } + +func paerserDurationPtr(duration int) *ptypes.Duration { + d := ptypes.Duration(duration) + return &d +} diff --git a/pkg/api/testdata/entrypoints.json b/pkg/api/testdata/entrypoints.json index d93d07bfc..93f56a401 100644 --- a/pkg/api/testdata/entrypoints.json +++ b/pkg/api/testdata/entrypoints.json @@ -23,9 +23,11 @@ "requestAcceptGraceTimeout": "1ns" }, "respondingTimeouts": { - "idleTimeout": "5ns", - "readTimeout": "3ns", - "writeTimeout": "4ns" + "http": { + "idleTimeout": "5ns", + "readTimeout": "3ns", + "writeTimeout": "4ns" + } } } }, @@ -53,9 +55,11 @@ "requestAcceptGraceTimeout": "10ns" }, "respondingTimeouts": { - "idleTimeout": "50ns", - "readTimeout": "30ns", - "writeTimeout": "40ns" + "http": { + "idleTimeout": "5ns", + "readTimeout": "3ns", + "writeTimeout": "4ns" + } } } } diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index dcf9b3dba..2a47763a0 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -56,6 +56,9 @@ const ( // DefaultUDPTimeout defines how long to wait by default on an idle session, // before releasing all resources related to that session. DefaultUDPTimeout = 3 * time.Second + + // defaultLingeringTimeout defines the default maximum duration between each read operation on the connection. + defaultLingeringTimeout = 2 * time.Second ) // Configuration is the static configuration. @@ -118,16 +121,44 @@ func (a *API) SetDefaults() { a.Dashboard = true } -// RespondingTimeouts contains timeout configurations for incoming requests to the Traefik instance. +// RespondingTimeouts contains timeout configurations. type RespondingTimeouts struct { - ReadTimeout ptypes.Duration `description:"ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set." json:"readTimeout,omitempty" toml:"readTimeout,omitempty" yaml:"readTimeout,omitempty" export:"true"` - WriteTimeout ptypes.Duration `description:"WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set." json:"writeTimeout,omitempty" toml:"writeTimeout,omitempty" yaml:"writeTimeout,omitempty" export:"true"` - IdleTimeout ptypes.Duration `description:"IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set." json:"idleTimeout,omitempty" toml:"idleTimeout,omitempty" yaml:"idleTimeout,omitempty" export:"true"` + // Deprecated: please use `respondingTimeouts.http.readTimeout` instead. + ReadTimeout *ptypes.Duration `description:"(Deprecated) ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set." json:"readTimeout,omitempty" toml:"readTimeout,omitempty" yaml:"readTimeout,omitempty" export:"true"` + // Deprecated: please use `respondingTimeouts.http.writeTimeout` instead. + WriteTimeout *ptypes.Duration `description:"(Deprecated) WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set." json:"writeTimeout,omitempty" toml:"writeTimeout,omitempty" yaml:"writeTimeout,omitempty" export:"true"` + // Deprecated: please use `respondingTimeouts.http.idleTimeout` instead. + IdleTimeout *ptypes.Duration `description:"(Deprecated) IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set." json:"idleTimeout,omitempty" toml:"idleTimeout,omitempty" yaml:"idleTimeout,omitempty" export:"true"` + + HTTP *HTTPRespondingTimeouts `description:"Defines the HTTP responding timeouts." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" export:"true"` + TCP *TCPRespondingTimeouts `description:"Defines the TCP responding timeouts." json:"tcp,omitempty" toml:"tcp,omitempty" yaml:"tcp,omitempty" export:"true"` +} + +// HTTPRespondingTimeouts contains HTTP timeout configurations for incoming requests to the Traefik instance. +type HTTPRespondingTimeouts struct { + ReadTimeout *ptypes.Duration `description:"ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set." json:"readTimeout,omitempty" toml:"readTimeout,omitempty" yaml:"readTimeout,omitempty" export:"true"` + WriteTimeout *ptypes.Duration `description:"WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set." json:"writeTimeout,omitempty" toml:"writeTimeout,omitempty" yaml:"writeTimeout,omitempty" export:"true"` + IdleTimeout *ptypes.Duration `description:"IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout is set." json:"idleTimeout,omitempty" toml:"idleTimeout,omitempty" yaml:"idleTimeout,omitempty" export:"true"` +} + +// TCPRespondingTimeouts contains TCP timeout configurations for client connections to the Traefik instance. +type TCPRespondingTimeouts struct { + LingeringTimeout ptypes.Duration `description:"LingeringTimeout is the maximum duration between each TCP read operation on the connection. If zero, no timeout is set." json:"lingeringTimeout,omitempty" toml:"lingeringTimeout,omitempty" yaml:"lingeringTimeout,omitempty" export:"true"` } // SetDefaults sets the default values. func (a *RespondingTimeouts) SetDefaults() { - a.IdleTimeout = ptypes.Duration(DefaultIdleTimeout) + noTimeout := ptypes.Duration(0) + defaultIdleTimeout := ptypes.Duration(DefaultIdleTimeout) + a.HTTP = &HTTPRespondingTimeouts{ + ReadTimeout: &noTimeout, + WriteTimeout: &noTimeout, + IdleTimeout: &defaultIdleTimeout, + } + + a.TCP = &TCPRespondingTimeouts{ + LingeringTimeout: ptypes.Duration(defaultLingeringTimeout), + } } // ForwardingTimeouts contains timeout configurations for forwarding requests to the backend servers. @@ -211,6 +242,39 @@ func (c *Configuration) SetEffectiveConfiguration() { c.EntryPoints["http"] = ep } + for _, entrypoint := range c.EntryPoints { + if entrypoint.Transport == nil || + entrypoint.Transport.RespondingTimeouts == nil { + continue + } + + respondingTimeouts := entrypoint.Transport.RespondingTimeouts + + if respondingTimeouts.ReadTimeout != nil && + respondingTimeouts.HTTP != nil && + respondingTimeouts.HTTP.ReadTimeout == nil { + log.WithoutContext().Warnf("Option `respondingTimeouts.readTimeout` is deprecated, please use `respondingTimeouts.http.readTimeout` instead.") + respondingTimeouts.HTTP.ReadTimeout = respondingTimeouts.ReadTimeout + respondingTimeouts.ReadTimeout = nil + } + + if respondingTimeouts.WriteTimeout != nil && + respondingTimeouts.HTTP != nil && + respondingTimeouts.HTTP.WriteTimeout == nil { + log.WithoutContext().Warnf("Option `respondingTimeouts.writeTimeout` is deprecated, please use `respondingTimeouts.http.writeTimeout` instead.") + respondingTimeouts.HTTP.WriteTimeout = respondingTimeouts.WriteTimeout + respondingTimeouts.WriteTimeout = nil + } + + if respondingTimeouts.IdleTimeout != nil && + respondingTimeouts.HTTP != nil && + respondingTimeouts.HTTP.IdleTimeout == nil { + log.WithoutContext().Warnf("Option `respondingTimeouts.idleTimeout` is deprecated, please use `respondingTimeouts.http.idleTimeout` instead.") + respondingTimeouts.HTTP.IdleTimeout = respondingTimeouts.IdleTimeout + respondingTimeouts.IdleTimeout = nil + } + } + // Creates the internal traefik entry point if needed if (c.API != nil && c.API.Insecure) || (c.Ping != nil && !c.Ping.ManualRouting && c.Ping.EntryPoint == DefaultInternalEntryPointName) || @@ -316,6 +380,31 @@ func (c *Configuration) ValidateConfiguration() error { return errors.New("Nomad provider cannot have both namespace and namespaces options configured") } + for epName, entrypoint := range c.EntryPoints { + if entrypoint.Transport == nil || + entrypoint.Transport.RespondingTimeouts == nil || + entrypoint.Transport.RespondingTimeouts.HTTP == nil { + continue + } + + respondingTimeouts := entrypoint.Transport.RespondingTimeouts + + if respondingTimeouts.ReadTimeout != nil && + respondingTimeouts.HTTP.ReadTimeout != nil { + return fmt.Errorf("entrypoint %q has `readTimeout` option is defined multiple times (`respondingTimeouts.readTimeout` is deprecated)", epName) + } + + if respondingTimeouts.WriteTimeout != nil && + respondingTimeouts.HTTP.WriteTimeout != nil { + return fmt.Errorf("entrypoint %q has `writeTimeout` option is defined multiple times (`respondingTimeouts.writeTimeout` is deprecated)", epName) + } + + if respondingTimeouts.IdleTimeout != nil && + respondingTimeouts.HTTP.IdleTimeout != nil { + return fmt.Errorf("entrypoint %q has `idleTimeout` option is defined multiple times (`respondingTimeouts.idleTimeout` is deprecated)", epName) + } + } + return nil } diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index 368f2d956..4cc080683 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -520,6 +520,8 @@ func TestDo_staticConfiguration(t *testing.T) { }, } + paerserDuration := ptypes.Duration(111 * time.Second) + config.EntryPoints = static.EntryPoints{ "foobar": { Address: "foo Address", @@ -529,9 +531,14 @@ func TestDo_staticConfiguration(t *testing.T) { GraceTimeOut: ptypes.Duration(111 * time.Second), }, RespondingTimeouts: &static.RespondingTimeouts{ - ReadTimeout: ptypes.Duration(111 * time.Second), - WriteTimeout: ptypes.Duration(111 * time.Second), - IdleTimeout: ptypes.Duration(111 * time.Second), + HTTP: &static.HTTPRespondingTimeouts{ + ReadTimeout: &paerserDuration, + WriteTimeout: &paerserDuration, + IdleTimeout: &paerserDuration, + }, + TCP: &static.TCPRespondingTimeouts{ + LingeringTimeout: ptypes.Duration(111 * time.Second), + }, }, }, ProxyProtocol: &static.ProxyProtocol{ diff --git a/pkg/redactor/testdata/anonymized-static-config.json b/pkg/redactor/testdata/anonymized-static-config.json index 09e0d796d..dd25c1fa3 100644 --- a/pkg/redactor/testdata/anonymized-static-config.json +++ b/pkg/redactor/testdata/anonymized-static-config.json @@ -26,9 +26,14 @@ "graceTimeOut": "1m51s" }, "respondingTimeouts": { - "readTimeout": "1m51s", - "writeTimeout": "1m51s", - "idleTimeout": "1m51s" + "http": { + "readTimeout": "1m51s", + "writeTimeout": "1m51s", + "idleTimeout": "1m51s" + }, + "tcp": { + "lingeringTimeout": "1m51s" + } } }, "proxyProtocol": { diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 0d8524398..551bb43e7 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -9,7 +9,6 @@ import ( "net" "net/http" "slices" - "time" "github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/traefik/traefik/v2/pkg/log" @@ -117,17 +116,6 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { return } - // Remove read/write deadline and delegate this to underlying tcp server (for now only handled by HTTP Server) - err = conn.SetReadDeadline(time.Time{}) - if err != nil { - log.WithoutContext().Errorf("Error while setting read deadline: %v", err) - } - - err = conn.SetWriteDeadline(time.Time{}) - if err != nil { - log.WithoutContext().Errorf("Error while setting write deadline: %v", err) - } - connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos) if err != nil { log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err) diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 1fb371d08..b5aaa1ac0 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -244,24 +244,15 @@ func (e *TCPEntryPoint) Start(ctx context.Context) { panic(err) } + if e.transportConfiguration != nil && + e.transportConfiguration.RespondingTimeouts != nil && + e.transportConfiguration.RespondingTimeouts.TCP != nil && + e.transportConfiguration.RespondingTimeouts.TCP.LingeringTimeout > 0 { + lingeringTimeout := time.Duration(e.transportConfiguration.RespondingTimeouts.TCP.LingeringTimeout) + writeCloser = newLingeringConnection(writeCloser, lingeringTimeout) + } + safe.Go(func() { - // Enforce read/write deadlines at the connection level, - // because when we're peeking the first byte to determine whether we are doing TLS, - // the deadlines at the server level are not taken into account. - if e.transportConfiguration.RespondingTimeouts.ReadTimeout > 0 { - err := writeCloser.SetReadDeadline(time.Now().Add(time.Duration(e.transportConfiguration.RespondingTimeouts.ReadTimeout))) - if err != nil { - logger.Errorf("Error while setting read deadline: %v", err) - } - } - - if e.transportConfiguration.RespondingTimeouts.WriteTimeout > 0 { - err = writeCloser.SetWriteDeadline(time.Now().Add(time.Duration(e.transportConfiguration.RespondingTimeouts.WriteTimeout))) - if err != nil { - logger.Errorf("Error while setting write deadline: %v", err) - } - } - e.switcher.ServeTCP(newTrackedConnection(writeCloser, e.tracker)) }) } @@ -391,6 +382,55 @@ func writeCloser(conn net.Conn) (tcp.WriteCloser, error) { } } +// lingeringConn represents a writeCloser with lingeringTimeout handling. +type lingeringConn struct { + tcp.WriteCloser + + lingeringTimeout time.Duration + + rdlMu sync.RWMutex + // readDeadline is the current readDeadline set by an upper caller. + // In case of HTTP, the HTTP go server manipulates deadlines on the connection. + readDeadline time.Time +} + +// newLingeringConnection returns the given writeCloser augmented with lingeringTimeout handling. +func newLingeringConnection(conn tcp.WriteCloser, timeout time.Duration) tcp.WriteCloser { + return &lingeringConn{ + WriteCloser: conn, + lingeringTimeout: timeout, + } +} + +// Read reads data from the connection and postpones the connection readDeadline according to the lingeringTimeout config. +// It also ensures that the upper level set readDeadline is enforced. +func (l *lingeringConn) Read(b []byte) (int, error) { + if l.lingeringTimeout > 0 { + deadline := time.Now().Add(l.lingeringTimeout) + + l.rdlMu.RLock() + if !l.readDeadline.IsZero() && deadline.After(l.readDeadline) { + deadline = l.readDeadline + } + l.rdlMu.RUnlock() + + if err := l.WriteCloser.SetReadDeadline(deadline); err != nil { + return 0, err + } + } + + return l.WriteCloser.Read(b) +} + +// SetReadDeadline sets and save the read deadline. +func (l *lingeringConn) SetReadDeadline(t time.Time) error { + l.rdlMu.Lock() + l.readDeadline = t + l.rdlMu.Unlock() + + return l.WriteCloser.SetReadDeadline(t) +} + // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // connections. type tcpKeepAliveListener struct { @@ -419,7 +459,7 @@ func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { } func buildProxyProtocolListener(ctx context.Context, entryPoint *static.EntryPoint, listener net.Listener) (net.Listener, error) { - timeout := entryPoint.Transport.RespondingTimeouts.ReadTimeout + timeout := *entryPoint.Transport.RespondingTimeouts.HTTP.ReadTimeout // proxyproto use 200ms if ReadHeaderTimeout is set to 0 and not no timeout if timeout == 0 { timeout = -1 @@ -588,9 +628,9 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati serverHTTP := &http.Server{ Handler: handler, ErrorLog: httpServerLogger, - ReadTimeout: time.Duration(configuration.Transport.RespondingTimeouts.ReadTimeout), - WriteTimeout: time.Duration(configuration.Transport.RespondingTimeouts.WriteTimeout), - IdleTimeout: time.Duration(configuration.Transport.RespondingTimeouts.IdleTimeout), + ReadTimeout: time.Duration(*configuration.Transport.RespondingTimeouts.HTTP.ReadTimeout), + WriteTimeout: time.Duration(*configuration.Transport.RespondingTimeouts.HTTP.WriteTimeout), + IdleTimeout: time.Duration(*configuration.Transport.RespondingTimeouts.HTTP.IdleTimeout), } if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) { serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context { diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index f0b12c8dd..6b177099f 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -69,8 +69,10 @@ func testShutdown(t *testing.T, router *tcprouter.Router) { epConfig.LifeCycle.RequestAcceptGraceTimeout = 0 epConfig.LifeCycle.GraceTimeOut = ptypes.Duration(5 * time.Second) - epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(5 * time.Second) - epConfig.RespondingTimeouts.WriteTimeout = ptypes.Duration(5 * time.Second) + readTimeout := ptypes.Duration(5 * time.Second) + epConfig.RespondingTimeouts.HTTP.ReadTimeout = &readTimeout + writeTimeout := ptypes.Duration(5 * time.Second) + epConfig.RespondingTimeouts.HTTP.WriteTimeout = &writeTimeout entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{ // We explicitly use an IPV4 address because on Alpine, with an IPV6 address @@ -157,7 +159,8 @@ func startEntrypoint(entryPoint *TCPEntryPoint, router *tcprouter.Router) (net.C func TestReadTimeoutWithoutFirstByte(t *testing.T) { epConfig := &static.EntryPointsTransport{} epConfig.SetDefaults() - epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(2 * time.Second) + readTimeout := ptypes.Duration(2 * time.Second) + epConfig.RespondingTimeouts.HTTP.ReadTimeout = &readTimeout entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{ Address: ":0", @@ -194,7 +197,84 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) { func TestReadTimeoutWithFirstByte(t *testing.T) { epConfig := &static.EntryPointsTransport{} epConfig.SetDefaults() - epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(2 * time.Second) + readTimeout := ptypes.Duration(2 * time.Second) + epConfig.RespondingTimeouts.HTTP.ReadTimeout = &readTimeout + + entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{ + Address: ":0", + Transport: epConfig, + ForwardedHeaders: &static.ForwardedHeaders{}, + HTTP2: &static.HTTP2Config{}, + }, nil) + require.NoError(t, err) + + router := &tcprouter.Router{} + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + conn, err := startEntrypoint(entryPoint, router) + require.NoError(t, err) + + _, err = conn.Write([]byte("GET /some HTTP/1.1\r\n")) + require.NoError(t, err) + + errChan := make(chan error) + + go func() { + b := make([]byte, 2048) + _, err := conn.Read(b) + errChan <- err + }() + + select { + case err := <-errChan: + require.Equal(t, io.EOF, err) + case <-time.Tick(5 * time.Second): + t.Error("Timeout while read") + } +} + +func TestLingeringTimeoutWithoutFirstByte(t *testing.T) { + epConfig := &static.EntryPointsTransport{} + epConfig.SetDefaults() + + entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{ + Address: ":0", + Transport: epConfig, + ForwardedHeaders: &static.ForwardedHeaders{}, + HTTP2: &static.HTTP2Config{}, + }, nil) + require.NoError(t, err) + + router := &tcprouter.Router{} + router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + + conn, err := startEntrypoint(entryPoint, router) + require.NoError(t, err) + + errChan := make(chan error) + + go func() { + b := make([]byte, 2048) + _, err := conn.Read(b) + errChan <- err + }() + + select { + case err := <-errChan: + require.Equal(t, io.EOF, err) + case <-time.Tick(5 * time.Second): + t.Error("Timeout while read") + } +} + +func TestLingeringTimeoutWithFirstByte(t *testing.T) { + epConfig := &static.EntryPointsTransport{} + epConfig.SetDefaults() + epConfig.RespondingTimeouts.TCP.LingeringTimeout = ptypes.Duration(time.Second) entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{ Address: ":0", From 76723b12883097568ff6a1e34a857195999023fc Mon Sep 17 00:00:00 2001 From: guangwu Date: Tue, 9 Apr 2024 19:12:04 +0800 Subject: [PATCH 08/30] Close created file in ACME local store CheckFile func --- pkg/provider/acme/local_store_unix.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/provider/acme/local_store_unix.go b/pkg/provider/acme/local_store_unix.go index f6d590536..163ecd7ae 100644 --- a/pkg/provider/acme/local_store_unix.go +++ b/pkg/provider/acme/local_store_unix.go @@ -11,14 +11,15 @@ import ( // CheckFile checks file permissions and content size. func CheckFile(name string) (bool, error) { f, err := os.Open(name) - if err != nil { - if os.IsNotExist(err) { - f, err = os.Create(name) - if err != nil { - return false, err - } - return false, f.Chmod(0o600) + if err != nil && os.IsNotExist(err) { + nf, err := os.Create(name) + if err != nil { + return false, err } + defer nf.Close() + return false, nf.Chmod(0o600) + } + if err != nil { return false, err } defer f.Close() From 0017471f0d643ecc53768839c24658183fa07e54 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Wed, 10 Apr 2024 09:34:07 +0200 Subject: [PATCH 09/30] Add option to set Gateway status address Co-authored-by: Romain --- docs/content/providers/kubernetes-gateway.md | 79 ++++++++++ .../reference/static-configuration/cli-ref.md | 15 ++ .../reference/static-configuration/env-ref.md | 15 ++ .../reference/static-configuration/file.toml | 6 + .../reference/static-configuration/file.yaml | 6 + .../kubernetes/gateway/fixtures/services.yml | 13 ++ pkg/provider/kubernetes/gateway/kubernetes.go | 81 +++++++++- .../kubernetes/gateway/kubernetes_test.go | 149 +++++++++++++++--- 8 files changed, 333 insertions(+), 31 deletions(-) diff --git a/docs/content/providers/kubernetes-gateway.md b/docs/content/providers/kubernetes-gateway.md index fe49dbe75..c01159904 100644 --- a/docs/content/providers/kubernetes-gateway.md +++ b/docs/content/providers/kubernetes-gateway.md @@ -212,6 +212,85 @@ providers: --providers.kubernetesgateway.namespaces=default,production ``` +### `statusAddress` + +#### `ip` + +_Optional, Default: ""_ + +This IP will get copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). + +```yaml tab="File (YAML)" +providers: + kubernetesGateway: + statusAddress: + ip: "1.2.3.4" + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesGateway.statusAddress] + ip = "1.2.3.4" + # ... +``` + +```bash tab="CLI" +--providers.kubernetesgateway.statusaddress.ip=1.2.3.4 +``` + +#### `hostname` + +_Optional, Default: ""_ + +This Hostname will get copied to the Gateway `status.addresses`. + +```yaml tab="File (YAML)" +providers: + kubernetesGateway: + statusAddress: + hostname: "example.net" + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesGateway.statusAddress] + hostname = "example.net" + # ... +``` + +```bash tab="CLI" +--providers.kubernetesgateway.statusaddress.hostname=example.net +``` + +#### `service` + +_Optional_ + +The Kubernetes service to copy status addresses from. +When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the gateways. + +```yaml tab="File (YAML)" +providers: + kubernetesGateway: + statusAddress: + service: + namespace: default + name: foo + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesGateway.statusAddress.service] + namespace = "default" + name = "foo" + # ... +``` + +```bash tab="CLI" +--providers.kubernetesgateway.statusaddress.service.namespace=default +--providers.kubernetesgateway.statusaddress.service.name=foo +``` + ### `experimentalChannel` _Optional, Default: false_ diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 6194dfbfe..6d49edb59 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -738,6 +738,21 @@ Kubernetes label selector to select specific GatewayClasses. `--providers.kubernetesgateway.namespaces`: Kubernetes namespaces. +`--providers.kubernetesgateway.statusaddress.hostname`: +Hostname used for Kubernetes Gateway status address. + +`--providers.kubernetesgateway.statusaddress.ip`: +IP used to set Kubernetes Gateway status address. + +`--providers.kubernetesgateway.statusaddress.service`: +Published Kubernetes Service to copy status addresses from. + +`--providers.kubernetesgateway.statusaddress.service.name`: +Name of the Kubernetes service. + +`--providers.kubernetesgateway.statusaddress.service.namespace`: +Namespace of the Kubernetes service. + `--providers.kubernetesgateway.throttleduration`: Kubernetes refresh throttle duration (Default: ```0```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 7fcde07fc..cac42b39b 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -738,6 +738,21 @@ Kubernetes label selector to select specific GatewayClasses. `TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_NAMESPACES`: Kubernetes namespaces. +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_HOSTNAME`: +Hostname used for Kubernetes Gateway status address. + +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_IP`: +IP used to set Kubernetes Gateway status address. + +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_SERVICE`: +Published Kubernetes Service to copy status addresses from. + +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_SERVICE_NAME`: +Name of the Kubernetes service. + +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_SERVICE_NAMESPACE`: +Namespace of the Kubernetes service. + `TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_THROTTLEDURATION`: Kubernetes refresh throttle duration (Default: ```0```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index e5a6e379e..9d796c4a6 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -147,6 +147,12 @@ labelSelector = "foobar" throttleDuration = "42s" experimentalChannel = true + [providers.kubernetesGateway.statusAddress] + ip = "foobar" + hostname = "foobar" + [providers.kubernetesGateway.statusAddress.service] + name = "foobar" + namespace = "foobar" [providers.rest] insecure = true [providers.consulCatalog] diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 362418fbc..477fa6b0e 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -164,6 +164,12 @@ providers: labelSelector: foobar throttleDuration: 42s experimentalChannel: true + statusAddress: + ip: foobar + hostname: foobar + service: + name: foobar + namespace: foobar rest: insecure: true consulCatalog: diff --git a/pkg/provider/kubernetes/gateway/fixtures/services.yml b/pkg/provider/kubernetes/gateway/fixtures/services.yml index 2dee27a3e..d9ebf1e6b 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/services.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/services.yml @@ -269,3 +269,16 @@ spec: - protocol: TCP port: 10000 name: tcp-2 + +--- +apiVersion: v1 +kind: Service +metadata: + name: status-address + namespace: default + +status: + loadBalancer: + ingress: + - hostname: foo.bar + - ip: 1.2.3.4 diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 4c355c3a5..590f840b4 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -58,6 +58,7 @@ type Provider struct { LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` ThrottleDuration ptypes.Duration `description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` ExperimentalChannel bool `description:"Toggles Experimental Channel resources support (TCPRoute, TLSRoute...)." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"` + StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"` EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` @@ -71,6 +72,19 @@ type Provider struct { routerTransform k8s.RouterTransform } +// StatusAddress holds the Gateway Status address configuration. +type StatusAddress struct { + IP string `description:"IP used to set Kubernetes Gateway status address." json:"ip,omitempty" toml:"ip,omitempty" yaml:"ip,omitempty"` + Hostname string `description:"Hostname used for Kubernetes Gateway status address." json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"` + Service ServiceRef `description:"Published Kubernetes Service to copy status addresses from." json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty"` +} + +// ServiceRef holds a Kubernetes service reference. +type ServiceRef struct { + Name string `description:"Name of the Kubernetes service." json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"` + Namespace string `description:"Namespace of the Kubernetes service." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` +} + // BuildFilterFunc returns the name of the filter and the related dynamic.Middleware if needed. type BuildFilterFunc func(name, namespace string) (string, *dynamic.Middleware, error) @@ -368,9 +382,14 @@ func (p *Provider) createGatewayConf(ctx context.Context, client Client, gateway // and cannot be configured on the Gateway. listenerStatuses := p.fillGatewayConf(ctx, client, gateway, conf, tlsConfigs) - gatewayStatus, errG := p.makeGatewayStatus(gateway, listenerStatuses) + addresses, err := p.gatewayAddresses(client) + if err != nil { + return nil, fmt.Errorf("get Gateway status addresses: %w", err) + } - err := client.UpdateGatewayStatus(gateway, gatewayStatus) + gatewayStatus, errG := p.makeGatewayStatus(gateway, listenerStatuses, addresses) + + err = client.UpdateGatewayStatus(gateway, gatewayStatus) if err != nil { return nil, fmt.Errorf("an error occurred while updating gateway status: %w", err) } @@ -618,11 +637,8 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * return listenerStatuses } -func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses []gatev1.ListenerStatus) (gatev1.GatewayStatus, error) { - // As Status.Addresses are not implemented yet, we initialize an empty array to follow the API expectations. - gatewayStatus := gatev1.GatewayStatus{ - Addresses: []gatev1.GatewayStatusAddress{}, - } +func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses []gatev1.ListenerStatus, addresses []gatev1.GatewayStatusAddress) (gatev1.GatewayStatus, error) { + gatewayStatus := gatev1.GatewayStatus{Addresses: addresses} var result error for i, listener := range listenerStatuses { @@ -701,6 +717,57 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [ return gatewayStatus, nil } +func (p *Provider) gatewayAddresses(client Client) ([]gatev1.GatewayStatusAddress, error) { + if p.StatusAddress == nil { + return nil, nil + } + + if p.StatusAddress.IP != "" { + return []gatev1.GatewayStatusAddress{{ + Type: ptr.To(gatev1.IPAddressType), + Value: p.StatusAddress.IP, + }}, nil + } + + if p.StatusAddress.Hostname != "" { + return []gatev1.GatewayStatusAddress{{ + Type: ptr.To(gatev1.HostnameAddressType), + Value: p.StatusAddress.Hostname, + }}, nil + } + + svcRef := p.StatusAddress.Service + if svcRef.Name != "" && svcRef.Namespace != "" { + svc, exists, err := client.GetService(svcRef.Namespace, svcRef.Name) + if err != nil { + return nil, fmt.Errorf("unable to get service: %w", err) + } + if !exists { + return nil, fmt.Errorf("could not find a service with name %s in namespace %s", svcRef.Name, svcRef.Namespace) + } + + var addresses []gatev1.GatewayStatusAddress + for _, addr := range svc.Status.LoadBalancer.Ingress { + switch { + case addr.IP != "": + addresses = append(addresses, gatev1.GatewayStatusAddress{ + Type: ptr.To(gatev1.IPAddressType), + Value: addr.IP, + }) + + case addr.Hostname != "": + addresses = append(addresses, gatev1.GatewayStatusAddress{ + Type: ptr.To(gatev1.HostnameAddressType), + Value: addr.Hostname, + }) + } + } + return addresses, nil + } + + return nil, errors.New("empty Gateway status address configuration") +} + func (p *Provider) entryPointName(port gatev1.PortNumber, protocol gatev1.ProtocolType) (string, error) { portStr := strconv.FormatInt(int64(port), 10) diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 64f7c7f96..bfc9e3072 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -6296,30 +6296,6 @@ func Test_makeListenerKey(t *testing.T) { } } -func hostnamePtr(hostname gatev1.Hostname) *gatev1.Hostname { - return &hostname -} - -func groupPtr(group gatev1.Group) *gatev1.Group { - return &group -} - -func sectionNamePtr(sectionName gatev1.SectionName) *gatev1.SectionName { - return §ionName -} - -func namespacePtr(namespace gatev1.Namespace) *gatev1.Namespace { - return &namespace -} - -func kindPtr(kind gatev1.Kind) *gatev1.Kind { - return &kind -} - -func pathMatchTypePtr(p gatev1.PathMatchType) *gatev1.PathMatchType { return &p } - -func headerMatchTypePtr(h gatev1.HeaderMatchType) *gatev1.HeaderMatchType { return &h } - func Test_referenceGrantMatchesFrom(t *testing.T) { testCases := []struct { desc string @@ -6558,6 +6534,131 @@ func Test_referenceGrantMatchesTo(t *testing.T) { } } +func Test_gatewayAddresses(t *testing.T) { + testCases := []struct { + desc string + statusAddress *StatusAddress + paths []string + wantErr require.ErrorAssertionFunc + want []gatev1.GatewayStatusAddress + }{ + { + desc: "nothing", + wantErr: require.NoError, + }, + { + desc: "empty configuration", + statusAddress: &StatusAddress{}, + wantErr: require.Error, + }, + { + desc: "IP address", + statusAddress: &StatusAddress{ + IP: "1.2.3.4", + }, + wantErr: require.NoError, + want: []gatev1.GatewayStatusAddress{ + { + Type: ptr.To(gatev1.IPAddressType), + Value: "1.2.3.4", + }, + }, + }, + { + desc: "hostname address", + statusAddress: &StatusAddress{ + Hostname: "foo.bar", + }, + wantErr: require.NoError, + want: []gatev1.GatewayStatusAddress{ + { + Type: ptr.To(gatev1.HostnameAddressType), + Value: "foo.bar", + }, + }, + }, + { + desc: "service", + statusAddress: &StatusAddress{ + Service: ServiceRef{ + Name: "status-address", + Namespace: "default", + }, + }, + paths: []string{"services.yml"}, + wantErr: require.NoError, + want: []gatev1.GatewayStatusAddress{ + { + Type: ptr.To(gatev1.HostnameAddressType), + Value: "foo.bar", + }, + { + Type: ptr.To(gatev1.IPAddressType), + Value: "1.2.3.4", + }, + }, + }, + { + desc: "missing service", + statusAddress: &StatusAddress{ + Service: ServiceRef{ + Name: "status-address2", + Namespace: "default", + }, + }, + wantErr: require.Error, + }, + { + desc: "service without load-balancer status", + statusAddress: &StatusAddress{ + Service: ServiceRef{ + Name: "whoamitcp-bar", + Namespace: "bar", + }, + }, + paths: []string{"services.yml"}, + wantErr: require.NoError, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := Provider{StatusAddress: test.statusAddress} + + got, err := p.gatewayAddresses(newClientMock(test.paths...)) + test.wantErr(t, err) + + assert.Equal(t, test.want, got) + }) + } +} + +func hostnamePtr(hostname gatev1.Hostname) *gatev1.Hostname { + return &hostname +} + +func groupPtr(group gatev1.Group) *gatev1.Group { + return &group +} + +func sectionNamePtr(sectionName gatev1.SectionName) *gatev1.SectionName { + return §ionName +} + +func namespacePtr(namespace gatev1.Namespace) *gatev1.Namespace { + return &namespace +} + +func kindPtr(kind gatev1.Kind) *gatev1.Kind { + return &kind +} + +func pathMatchTypePtr(p gatev1.PathMatchType) *gatev1.PathMatchType { return &p } + +func headerMatchTypePtr(h gatev1.HeaderMatchType) *gatev1.HeaderMatchType { return &h } + func objectNamePtr(objectName gatev1.ObjectName) *gatev1.ObjectName { return &objectName } From 19e6170fa5fea248268fb60eac1257702be75a72 Mon Sep 17 00:00:00 2001 From: Massimiliano D <126668030+mdeliatf@users.noreply.github.com> Date: Wed, 10 Apr 2024 09:50:04 +0200 Subject: [PATCH 10/30] Modify the Hub Button --- webui/public/traefiklabs-hub-button-app/main-v1.js | 4 ++-- webui/public/traefiklabs-hub-button-app/main-v1.js.map | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/webui/public/traefiklabs-hub-button-app/main-v1.js b/webui/public/traefiklabs-hub-button-app/main-v1.js index 9d36a8b62..2c912be53 100644 --- a/webui/public/traefiklabs-hub-button-app/main-v1.js +++ b/webui/public/traefiklabs-hub-button-app/main-v1.js @@ -1,3 +1,3 @@ /* eslint-disable */ -!function(){var e={110:function(e,t,n){"use strict";var r=n(441),a={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},l={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},o={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},i={};function u(e){return r.isMemo(e)?o:i[e.$$typeof]||a}i[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},i[r.Memo]=o;var s=Object.defineProperty,c=Object.getOwnPropertyNames,f=Object.getOwnPropertySymbols,d=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,h=Object.prototype;e.exports=function e(t,n,r){if("string"!==typeof n){if(h){var a=p(n);a&&a!==h&&e(t,a,r)}var o=c(n);f&&(o=o.concat(f(n)));for(var i=u(t),m=u(n),g=0;g