From 3315a9fbec5ffd61ebfd200e6af4c53f7a780b19 Mon Sep 17 00:00:00 2001 From: mmatur Date: Thu, 15 Jan 2026 11:26:40 +0100 Subject: [PATCH] Merge current v2.11 into v3.6 --- .github/workflows/validate.yaml | 2 +- .golangci.yml | 22 +- cmd/configuration.go | 1 + cmd/internal/gen/main.go | 2 +- .../kubernetes-crd-definition-v1.yml | 11 +- .../traefik.containo.us_tlsoptions.yaml | 114 ---------- .../traefik.io_ingressroutes.yaml | 1 + .../traefik.io_ingressroutetcps.yaml | 3 + .../traefik.io_middlewares.yaml | 1 + .../traefik.io_middlewaretcps.yaml | 3 +- .../traefik.io_serverstransports.yaml | 1 + .../traefik.io_serverstransporttcps.yaml | 1 + .../traefik.io_tlsoptions.yaml | 1 + integration/access_log_test.go | 3 +- integration/acme_test.go | 11 +- integration/consul_catalog_test.go | 83 ++++---- integration/consul_test.go | 21 +- integration/docker_test.go | 6 +- integration/dual_logging_test.go | 1 + integration/error_pages_test.go | 1 + integration/etcd_test.go | 1 + integration/fixtures/k8s/01-traefik-crd.yml | 11 +- integration/healthcheck_test.go | 1 + integration/helloworld/helloworld.pb.go | 6 +- integration/https_test.go | 68 +++--- integration/integration_test.go | 73 +++---- integration/log_rotation_test.go | 1 - integration/proxy_protocol_test.go | 9 +- integration/ratelimit_test.go | 1 + integration/redis_sentinel_test.go | 61 +++--- integration/redis_test.go | 1 + integration/rest_test.go | 1 + integration/retry_test.go | 1 + integration/tcp_healthcheck_test.go | 1 + integration/tracing_test.go | 1 + integration/try/try.go | 6 +- integration/zk_test.go | 1 + internal/gendoc.go | 2 +- pkg/api/criterion.go | 13 +- pkg/api/debug.go | 2 +- pkg/api/handler.go | 4 +- pkg/api/handler_entrypoint.go | 1 + pkg/api/handler_entrypoint_test.go | 2 +- pkg/api/handler_http.go | 3 + pkg/api/handler_http_test.go | 2 +- pkg/api/handler_overview_test.go | 4 +- pkg/api/handler_tcp.go | 3 + pkg/api/handler_tcp_test.go | 2 +- pkg/api/handler_test.go | 2 +- pkg/api/handler_udp.go | 2 + pkg/api/handler_udp_test.go | 2 +- pkg/cli/deprecation.go | 12 +- pkg/cli/loader_file.go | 2 +- pkg/collector/hydratation/hydration.go | 4 +- pkg/config/dynamic/config_test.go | 4 +- pkg/config/dynamic/middlewares.go | 2 + pkg/config/dynamic/plugins_test.go | 6 +- pkg/config/dynamic/tcp_config.go | 2 + pkg/config/dynamic/tcp_middlewares.go | 1 + pkg/config/kv/kv.go | 4 +- pkg/config/label/label.go | 2 +- pkg/config/runtime/runtime_http.go | 28 +-- pkg/config/runtime/runtime_tcp.go | 34 ++- pkg/config/runtime/runtime_udp.go | 22 +- pkg/config/static/plugins.go | 2 +- pkg/config/static/static_config.go | 30 +-- pkg/healthcheck/healthcheck.go | 2 +- pkg/healthcheck/mock_test.go | 1 + pkg/job/job.go | 1 + .../accesslog/field_middleware_test.go | 2 +- pkg/middlewares/accesslog/logdata.go | 2 +- pkg/middlewares/accesslog/logger.go | 102 ++++----- .../accesslog/logger_formatters.go | 2 +- .../accesslog/logger_formatters_test.go | 18 +- pkg/middlewares/accesslog/logger_test.go | 42 ++-- pkg/middlewares/auth/connectionheader.go | 2 +- pkg/middlewares/capture/capture.go | 28 +-- pkg/middlewares/compress/acceptencoding.go | 2 +- pkg/middlewares/compress/compress.go | 8 +- .../compress/compression_handler.go | 1 + .../compress/compression_handler_test.go | 10 +- pkg/middlewares/customerrors/custom_errors.go | 39 ++-- .../forwardedheaders/forwarded_header.go | 50 ++--- .../request_header_modifier_test.go | 5 +- .../response_header_modifier_test.go | 5 +- pkg/middlewares/headers/header.go | 42 ++-- .../observability/observability.go | 2 +- pkg/middlewares/observability/status_code.go | 1 + pkg/middlewares/ratelimiter/lua.go | 8 +- pkg/middlewares/ratelimiter/rate_limiter.go | 5 +- .../ratelimiter/rate_limiter_test.go | 24 +-- pkg/middlewares/ratelimiter/redis_limiter.go | 4 +- pkg/middlewares/recovery/recovery.go | 2 +- pkg/middlewares/retry/retry.go | 6 +- pkg/muxer/http/matcher.go | 16 +- pkg/muxer/http/mux.go | 3 +- pkg/muxer/tcp/matcher.go | 9 +- pkg/muxer/tcp/matcher_v2.go | 7 +- pkg/muxer/tcp/mux.go | 2 +- pkg/observability/logs/aws.go | 4 +- pkg/observability/logs/elastic.go | 4 +- pkg/observability/logs/gokit.go | 4 +- pkg/observability/logs/hclog.go | 10 +- pkg/observability/logs/instana.go | 8 +- pkg/observability/logs/logrus.go | 18 +- pkg/observability/logs/oxy.go | 8 +- pkg/observability/metrics/headers.go | 10 +- pkg/observability/metrics/otel.go | 2 +- pkg/observability/tracing/tracing.go | 140 ++++++------- pkg/observability/tracing/tracing_test.go | 2 + pkg/plugins/builder.go | 4 +- pkg/plugins/manager_test.go | 2 +- pkg/plugins/middlewarewasm.go | 4 +- pkg/plugins/middlewarewasm_test.go | 18 +- pkg/plugins/middlewareyaegi.go | 4 +- pkg/plugins/middlewareyaegi_test.go | 2 +- pkg/plugins/providers.go | 6 +- pkg/plugins/types.go | 20 +- pkg/provider/acme/local_store.go | 102 +++++---- pkg/provider/acme/local_store_unix.go | 1 - pkg/provider/acme/provider.go | 4 +- pkg/provider/aggregator/aggregator.go | 14 +- pkg/provider/configuration.go | 6 +- .../constraints/constraints_labels.go | 2 +- pkg/provider/constraints/constraints_tags.go | 2 +- pkg/provider/consulcatalog/consul_catalog.go | 132 ++++++------ pkg/provider/docker/config.go | 1 + pkg/provider/docker/pdocker.go | 8 +- pkg/provider/docker/pswarm.go | 8 +- pkg/provider/docker/pswarm_mock_test.go | 2 + pkg/provider/ecs/ecs.go | 70 +++---- pkg/provider/file/file.go | 153 +++++++------- pkg/provider/kubernetes/crd/client.go | 6 +- pkg/provider/kubernetes/crd/kubernetes.go | 196 +++++++++--------- .../kubernetes/crd/kubernetes_test.go | 22 +- .../crd/traefikio/v1alpha1/ingressroute.go | 1 + .../crd/traefikio/v1alpha1/ingressroutetcp.go | 3 + .../crd/traefikio/v1alpha1/middlewaretcp.go | 3 +- .../traefikio/v1alpha1/serverstransport.go | 1 + .../traefikio/v1alpha1/serverstransporttcp.go | 1 + .../crd/traefikio/v1alpha1/tlsoption.go | 1 + pkg/provider/kubernetes/gateway/client.go | 4 +- pkg/provider/kubernetes/gateway/httproute.go | 17 +- pkg/provider/kubernetes/gateway/kubernetes.go | 90 ++++---- pkg/provider/kubernetes/gateway/tcproute.go | 13 +- .../kubernetes/ingress-nginx/annotations.go | 5 +- .../kubernetes/ingress-nginx/client.go | 4 +- .../kubernetes/ingress-nginx/kubernetes.go | 7 +- pkg/provider/kubernetes/ingress/client.go | 6 +- .../kubernetes/ingress/client_mock_test.go | 4 +- pkg/provider/kubernetes/ingress/kubernetes.go | 116 +++++------ pkg/provider/kubernetes/k8s/event_handler.go | 12 +- .../kubernetes/k8s/event_handler_test.go | 4 +- pkg/provider/kubernetes/knative/client.go | 12 +- pkg/provider/kubernetes/knative/kubernetes.go | 20 +- pkg/provider/nomad/nomad.go | 10 +- pkg/proxy/fast/proxy.go | 2 +- pkg/proxy/httputil/bufferpool.go | 2 +- pkg/redactor/redactor.go | 12 +- pkg/rules/parser.go | 9 +- pkg/safe/routine.go | 4 +- pkg/safe/routine_test.go | 1 + pkg/safe/safe.go | 8 +- pkg/server/middleware/plugins.go | 6 +- pkg/server/router/router.go | 192 ++++++++--------- pkg/server/router/router_test.go | 4 +- pkg/server/router/tcp/manager.go | 52 ++--- pkg/server/router/tcp/router.go | 75 +++---- pkg/server/router/tcp/router_test.go | 11 +- pkg/server/router/udp/router.go | 28 +-- pkg/server/server_entrypoint_tcp.go | 38 ++-- pkg/server/server_entrypoint_tcp_http3.go | 10 +- pkg/server/server_signals.go | 1 - pkg/server/server_signals_windows.go | 1 - .../loadbalancer/failover/failover_test.go | 1 + pkg/server/service/loadbalancer/hrw/hrw.go | 63 +++--- .../service/loadbalancer/hrw/hrw_test.go | 1 + .../loadbalancer/leasttime/leasttime.go | 185 +++++++++-------- .../loadbalancer/leasttime/leasttime_test.go | 1 + .../service/loadbalancer/mirror/mirror.go | 49 ++--- pkg/server/service/loadbalancer/p2c/p2c.go | 88 ++++---- .../service/loadbalancer/p2c/p2c_test.go | 1 + pkg/server/service/loadbalancer/wrr/wrr.go | 65 +++--- .../service/loadbalancer/wrr/wrr_test.go | 1 + pkg/server/service/service.go | 26 +-- pkg/tcp/dialer.go | 1 + pkg/tcp/proxy_unix.go | 1 - pkg/tcp/proxy_windows.go | 1 - pkg/tcp/wrr_load_balancer.go | 1 + pkg/testhelpers/metrics.go | 10 +- pkg/tls/certificate_store.go | 4 +- pkg/tls/tls.go | 3 +- pkg/tls/tlsmanager.go | 16 +- pkg/udp/conn.go | 107 +++++----- pkg/udp/wrr_load_balancer.go | 1 + 195 files changed, 1748 insertions(+), 1852 deletions(-) delete mode 100644 docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index ed46d1599..e81b4aa99 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -7,7 +7,7 @@ on: env: GO_VERSION: '1.24' - GOLANGCI_LINT_VERSION: v2.0.2 + GOLANGCI_LINT_VERSION: v2.8.0 MISSPELL_VERSION: v0.7.0 jobs: diff --git a/.golangci.yml b/.golangci.yml index 50afac812..d49dcdcae 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -36,6 +36,7 @@ linters: - nilnil # Not relevant - nlreturn # Not relevant - noctx # Too strict + - noinlineerr # Too strict - nonamedreturns # Too strict - paralleltest # Not relevant - prealloc # Too many false-positive. @@ -47,6 +48,7 @@ linters: - varnamelen # Not relevant - wrapcheck # Too strict - wsl # Too strict + - wsl_v5 # Too strict settings: depguard: @@ -295,15 +297,31 @@ linters: source: 'errors.New\("Nomad provider' text: 'ST1005: error strings should not be capitalized' - path: (.+)\.go - text: 'struct-tag: unknown option ''inline'' in JSON tag' + text: 'omitzero: Omitempty has no effect on nested struct field' + linters: + - modernize + - path: (.+)\.go + text: 'struct-tag: unknown option "inline" in json tag' linters: - revive - path: (.+)\.go - text: 'struct-tag: unknown option ''omitzero'' in TOML tag' + text: 'struct-tag: unknown option "omitzero" in toml tag' + linters: + - revive + - path: (pkg/types/.+|pkg/api/.+|pkg/observability/types/.+)\.go + text: 'var-naming: avoid meaningless package names' + linters: + - revive + - path: (pkg/muxer/http/.+|pkg/provider/http/.+)\.go + text: 'var-naming: avoid package names that conflict with Go standard library package names' linters: - revive - path: (.+)\.go$ text: 'SA1019: http.CloseNotifier has been deprecated' # FIXME must be fixed + - path: (.+)\.go$ + text: 'SA1019: dynamic.(TCPIPWhiteList|IPWhiteList) is deprecated: please use IPAllowList instead.' + - path: (.+)\.go$ + text: 'SA1019: middlewareTCP.Spec.IPWhiteList is deprecated: please use IPAllowList instead.' - path: (.+)\.go$ text: 'SA1019: cfg.(SSLRedirect|SSLTemporaryRedirect|SSLHost|SSLForceHost|FeaturePolicy) is deprecated' - path: (.+)\.go$ diff --git a/cmd/configuration.go b/cmd/configuration.go index d65bdf04d..49a69507d 100644 --- a/cmd/configuration.go +++ b/cmd/configuration.go @@ -10,6 +10,7 @@ import ( // TraefikCmdConfiguration wraps the static configuration and extra parameters. type TraefikCmdConfiguration struct { static.Configuration `export:"true"` + // ConfigFile is the path to the configuration file. ConfigFile string `description:"Configuration file to use. If specified all other flags are ignored." export:"true"` } diff --git a/cmd/internal/gen/main.go b/cmd/internal/gen/main.go index a85544688..cacc64fb1 100644 --- a/cmd/internal/gen/main.go +++ b/cmd/internal/gen/main.go @@ -83,7 +83,7 @@ func run(dest string) error { return err } - return os.WriteFile(filepath.Join(dest, "marshaler.go"), []byte(fmt.Sprintf(marsh, destPkg)), 0o666) + return os.WriteFile(filepath.Join(dest, "marshaler.go"), fmt.Appendf(nil, marsh, destPkg), 0o666) } func cleanType(typ types.Type, base string) string { 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 763d981eb..6634b09b2 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -373,6 +373,7 @@ spec: description: |- Syntax defines the router's rule syntax. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/http/routing/rules-and-priority/#rulesyntax + Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. type: string required: @@ -584,6 +585,7 @@ spec: description: |- ProxyProtocol defines the PROXY protocol configuration. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/service/#proxy-protocol + Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead. properties: version: @@ -606,6 +608,7 @@ spec: hence fully terminating the connection. It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). + Deprecated: TerminationDelay will not be supported in future APIVersions, please use ServersTransport to configure the TerminationDelay instead. type: integer tls: @@ -626,6 +629,7 @@ spec: description: |- Syntax defines the router's rule syntax. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/routing/rules-and-priority/#rulesyntax + Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. enum: - v3 @@ -1060,6 +1064,7 @@ spec: description: |- AutoDetect specifies whether to let the `Content-Type` header, if it has not been set by the backend, be automatically set to a value derived from the contents of the response. + Deprecated: AutoDetect option is deprecated, Content-Type middleware is only meant to be used to enable the content-type detection, please remove any usage of this option. type: boolean type: object @@ -2212,8 +2217,9 @@ spec: description: |- IPWhiteList defines the IPWhiteList middleware configuration. This middleware accepts/refuses connections based on the client IP. - Deprecated: please use IPAllowList instead. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/middlewares/ipwhitelist/ + + Deprecated: please use IPAllowList instead. properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of @@ -2370,6 +2376,7 @@ spec: rootCAsSecrets: description: |- RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string @@ -2524,6 +2531,7 @@ spec: rootCAsSecrets: description: |- RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string @@ -2659,6 +2667,7 @@ spec: description: |- PreferServerCipherSuites defines whether the server chooses a cipher suite among his own instead of among the client's. It is enabled automatically when minVersion or maxVersion is set. + Deprecated: https://github.com/golang/go/issues/45430 type: boolean sniStrict: diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml deleted file mode 100644 index 6c7fdc914..000000000 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml +++ /dev/null @@ -1,114 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.1 - name: tlsoptions.traefik.containo.us -spec: - group: traefik.containo.us - names: - kind: TLSOption - listKind: TLSOptionList - plural: tlsoptions - singular: tlsoption - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: |- - TLSOption is the CRD implementation of a Traefik TLS Option, allowing to configure some parameters of the TLS connection. - More info: https://doc.traefik.io/traefik/v2.11/https/tls/#tls-options - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: TLSOptionSpec defines the desired state of a TLSOption. - properties: - alpnProtocols: - description: |- - ALPNProtocols defines the list of supported application level protocols for the TLS handshake, in order of preference. - More info: https://doc.traefik.io/traefik/v2.11/https/tls/#alpn-protocols - items: - type: string - type: array - cipherSuites: - description: |- - CipherSuites defines the list of supported cipher suites for TLS versions up to TLS 1.2. - More info: https://doc.traefik.io/traefik/v2.11/https/tls/#cipher-suites - items: - type: string - type: array - clientAuth: - description: ClientAuth defines the server's policy for TLS Client - Authentication. - properties: - clientAuthType: - description: ClientAuthType defines the client authentication - type to apply. - enum: - - NoClientCert - - RequestClientCert - - RequireAnyClientCert - - VerifyClientCertIfGiven - - RequireAndVerifyClientCert - type: string - secretNames: - description: SecretNames defines the names of the referenced Kubernetes - Secret storing certificate details. - items: - type: string - type: array - type: object - curvePreferences: - description: |- - CurvePreferences defines the preferred elliptic curves. - More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences - items: - type: string - type: array - maxVersion: - description: |- - MaxVersion defines the maximum TLS version that Traefik will accept. - Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13. - Default: None. - type: string - minVersion: - description: |- - MinVersion defines the minimum TLS version that Traefik will accept. - Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13. - Default: VersionTLS10. - type: string - preferServerCipherSuites: - description: |- - PreferServerCipherSuites defines whether the server chooses a cipher suite among his own instead of among the client's. - It is enabled automatically when minVersion or maxVersion is set. - Deprecated: https://github.com/golang/go/issues/45430 - type: boolean - sniStrict: - description: SniStrict defines whether Traefik allows connections - from clients connections that do not specify a server_name extension. - type: boolean - type: object - required: - - metadata - - spec - type: object - served: true - storage: true diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index 3debb5926..19aa7b3f2 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -374,6 +374,7 @@ spec: description: |- Syntax defines the router's rule syntax. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/http/routing/rules-and-priority/#rulesyntax + Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. type: string required: diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml index c7006f6d0..e70f8ac20 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml @@ -123,6 +123,7 @@ spec: description: |- ProxyProtocol defines the PROXY protocol configuration. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/service/#proxy-protocol + Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead. properties: version: @@ -145,6 +146,7 @@ spec: hence fully terminating the connection. It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). + Deprecated: TerminationDelay will not be supported in future APIVersions, please use ServersTransport to configure the TerminationDelay instead. type: integer tls: @@ -165,6 +167,7 @@ spec: description: |- Syntax defines the router's rule syntax. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/routing/rules-and-priority/#rulesyntax + Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. enum: - v3 diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index f0999cf22..e5ececaec 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -231,6 +231,7 @@ spec: description: |- AutoDetect specifies whether to let the `Content-Type` header, if it has not been set by the backend, be automatically set to a value derived from the contents of the response. + Deprecated: AutoDetect option is deprecated, Content-Type middleware is only meant to be used to enable the content-type detection, please remove any usage of this option. type: boolean type: object diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewaretcps.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewaretcps.yaml index b2a19f9d5..e68050d2b 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewaretcps.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewaretcps.yaml @@ -69,8 +69,9 @@ spec: description: |- IPWhiteList defines the IPWhiteList middleware configuration. This middleware accepts/refuses connections based on the client IP. - Deprecated: please use IPAllowList instead. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/middlewares/ipwhitelist/ + + Deprecated: please use IPAllowList instead. properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of diff --git a/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml b/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml index d5ec61903..fb8f24f89 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml @@ -139,6 +139,7 @@ spec: rootCAsSecrets: description: |- RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string diff --git a/docs/content/reference/dynamic-configuration/traefik.io_serverstransporttcps.yaml b/docs/content/reference/dynamic-configuration/traefik.io_serverstransporttcps.yaml index 79469823b..c4104f8a5 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_serverstransporttcps.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_serverstransporttcps.yaml @@ -124,6 +124,7 @@ spec: rootCAsSecrets: description: |- RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string diff --git a/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml b/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml index 5f37cb023..1520612a4 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml @@ -103,6 +103,7 @@ spec: description: |- PreferServerCipherSuites defines whether the server chooses a cipher suite among his own instead of among the client's. It is enabled automatically when minVersion or maxVersion is set. + Deprecated: https://github.com/golang/go/issues/45430 type: boolean sniStrict: diff --git a/integration/access_log_test.go b/integration/access_log_test.go index 2f480b524..200e73360 100644 --- a/integration/access_log_test.go +++ b/integration/access_log_test.go @@ -245,8 +245,7 @@ func digestParts(resp *http.Response) map[string]string { result := map[string]string{} if len(resp.Header["Www-Authenticate"]) > 0 { wantedHeaders := []string{"nonce", "realm", "qop", "opaque"} - responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",") - for _, r := range responseHeaders { + for r := range strings.SplitSeq(resp.Header["Www-Authenticate"][0], ",") { for _, w := range wantedHeaders { if strings.Contains(r, w) { result[w] = strings.Split(r, `"`)[1] diff --git a/integration/acme_test.go b/integration/acme_test.go index a226ea88d..51173ebf2 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -27,6 +27,7 @@ import ( // ACME test suites. type AcmeSuite struct { BaseSuite + pebbleIP string fakeDNSServer *dns.Server } @@ -63,11 +64,6 @@ const ( wildcardDomain = "*.acme.wtf" ) -func (s *AcmeSuite) getAcmeURL() string { - return fmt.Sprintf("https://%s/dir", - net.JoinHostPort(s.pebbleIP, "14000")) -} - func setupPebbleRootCA() (*http.Transport, error) { path, err := filepath.Abs("fixtures/acme/ssl/pebble.minica.pem") if err != nil { @@ -540,3 +536,8 @@ func (s *AcmeSuite) retrieveAcmeCertificate(testCase acmeTestCase) { assert.Equal(s.T(), sub.expectedAlgorithm, gotPublicKeyAlgorithm) } } + +func (s *AcmeSuite) getAcmeURL() string { + return fmt.Sprintf("https://%s/dir", + net.JoinHostPort(s.pebbleIP, "14000")) +} diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 65bb89a5e..11a69c490 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -16,6 +16,7 @@ import ( type ConsulCatalogSuite struct { BaseSuite + consulClient *api.Client consulAgentClient *api.Client consulURL string @@ -53,47 +54,6 @@ func (s *ConsulCatalogSuite) TearDownSuite() { s.BaseSuite.TearDownSuite() } -func (s *ConsulCatalogSuite) waitToElectConsulLeader() error { - return try.Do(15*time.Second, func() error { - leader, err := s.consulClient.Status().Leader() - - if err != nil || len(leader) == 0 { - return fmt.Errorf("leader not found. %w", err) - } - - return nil - }) -} - -func (s *ConsulCatalogSuite) waitForConnectCA() error { - return try.Do(15*time.Second, func() error { - caroots, _, err := s.consulClient.Connect().CARoots(nil) - - if err != nil || len(caroots.Roots) == 0 { - return fmt.Errorf("connect CA not fully initialized. %w", err) - } - - return nil - }) -} - -func (s *ConsulCatalogSuite) registerService(reg *api.AgentServiceRegistration, onAgent bool) error { - client := s.consulClient - if onAgent { - client = s.consulAgentClient - } - - return client.Agent().ServiceRegister(reg) -} - -func (s *ConsulCatalogSuite) deregisterService(id string, onAgent bool) error { - client := s.consulClient - if onAgent { - client = s.consulAgentClient - } - return client.Agent().ServiceDeregister(id) -} - func (s *ConsulCatalogSuite) TestWithNotExposedByDefaultAndDefaultsSettings() { reg1 := &api.AgentServiceRegistration{ ID: "whoami1", @@ -837,3 +797,44 @@ func (s *ConsulCatalogSuite) TestConsulConnect_NotAware() { err = s.deregisterService("whoami1", false) require.NoError(s.T(), err) } + +func (s *ConsulCatalogSuite) waitToElectConsulLeader() error { + return try.Do(15*time.Second, func() error { + leader, err := s.consulClient.Status().Leader() + + if err != nil || len(leader) == 0 { + return fmt.Errorf("leader not found. %w", err) + } + + return nil + }) +} + +func (s *ConsulCatalogSuite) waitForConnectCA() error { + return try.Do(15*time.Second, func() error { + caroots, _, err := s.consulClient.Connect().CARoots(nil) + + if err != nil || len(caroots.Roots) == 0 { + return fmt.Errorf("connect CA not fully initialized. %w", err) + } + + return nil + }) +} + +func (s *ConsulCatalogSuite) registerService(reg *api.AgentServiceRegistration, onAgent bool) error { + client := s.consulClient + if onAgent { + client = s.consulAgentClient + } + + return client.Agent().ServiceRegister(reg) +} + +func (s *ConsulCatalogSuite) deregisterService(id string, onAgent bool) error { + client := s.consulClient + if onAgent { + client = s.consulAgentClient + } + return client.Agent().ServiceDeregister(id) +} diff --git a/integration/consul_test.go b/integration/consul_test.go index 31908556e..4b5ad5146 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -25,6 +25,7 @@ import ( // Consul test suites. type ConsulSuite struct { BaseSuite + kvClient store.Store consulURL string } @@ -162,16 +163,6 @@ func (s *ConsulSuite) TestSimpleConfiguration() { } } -func (s *ConsulSuite) assertWhoami(host string, expectedStatusCode int) { - req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil) - require.NoError(s.T(), err) - req.Host = host - - resp, err := try.ResponseUntilStatusCode(req, 15*time.Second, expectedStatusCode) - require.NoError(s.T(), err) - resp.Body.Close() -} - func (s *ConsulSuite) TestDeleteRootKey() { // This test case reproduce the issue: https://github.com/traefik/traefik/issues/8092 @@ -220,3 +211,13 @@ func (s *ConsulSuite) TestDeleteRootKey() { s.assertWhoami("kv1.localhost", http.StatusNotFound) s.assertWhoami("kv2.localhost", http.StatusNotFound) } + +func (s *ConsulSuite) assertWhoami(host string, expectedStatusCode int) { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil) + require.NoError(s.T(), err) + req.Host = host + + resp, err := try.ResponseUntilStatusCode(req, 15*time.Second, expectedStatusCode) + require.NoError(s.T(), err) + resp.Body.Close() +} diff --git a/integration/docker_test.go b/integration/docker_test.go index 08606b60b..8ae24d921 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -81,7 +81,7 @@ func (s *DockerSuite) TestDefaultDockerContainers() { body, err := io.ReadAll(resp.Body) require.NoError(s.T(), err) - var version map[string]interface{} + var version map[string]any assert.NoError(s.T(), json.Unmarshal(body, &version)) assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) @@ -145,7 +145,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels() { body, err := io.ReadAll(resp.Body) require.NoError(s.T(), err) - var version map[string]interface{} + var version map[string]any assert.NoError(s.T(), json.Unmarshal(body, &version)) assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) @@ -203,7 +203,7 @@ func (s *DockerSuite) TestRestartDockerContainers() { body, err := io.ReadAll(resp.Body) require.NoError(s.T(), err) - var version map[string]interface{} + var version map[string]any assert.NoError(s.T(), json.Unmarshal(body, &version)) assert.Equal(s.T(), "swarm/1.0.0", version["Version"]) diff --git a/integration/dual_logging_test.go b/integration/dual_logging_test.go index fef4cbd54..c2affa3e8 100644 --- a/integration/dual_logging_test.go +++ b/integration/dual_logging_test.go @@ -21,6 +21,7 @@ const traefikTestOTLPLogFile = "traefik_otlp.log" // DualLoggingSuite tests that both OTLP and stdout logging can work together. type DualLoggingSuite struct { BaseSuite + otlpLogs []string collector *httptest.Server } diff --git a/integration/error_pages_test.go b/integration/error_pages_test.go index f895216e0..db8f15647 100644 --- a/integration/error_pages_test.go +++ b/integration/error_pages_test.go @@ -14,6 +14,7 @@ import ( // ErrorPagesSuite test suites. type ErrorPagesSuite struct { BaseSuite + ErrorPageIP string BackendIP string } diff --git a/integration/etcd_test.go b/integration/etcd_test.go index 067c4d61c..718601fc4 100644 --- a/integration/etcd_test.go +++ b/integration/etcd_test.go @@ -23,6 +23,7 @@ import ( // etcd test suites. type EtcdSuite struct { BaseSuite + kvClient store.Store etcdAddr string } diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index bbf30fb63..dfd63fe83 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -374,6 +374,7 @@ spec: description: |- Syntax defines the router's rule syntax. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/http/routing/rules-and-priority/#rulesyntax + Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. type: string required: @@ -585,6 +586,7 @@ spec: description: |- ProxyProtocol defines the PROXY protocol configuration. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/service/#proxy-protocol + Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead. properties: version: @@ -607,6 +609,7 @@ spec: hence fully terminating the connection. It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). + Deprecated: TerminationDelay will not be supported in future APIVersions, please use ServersTransport to configure the TerminationDelay instead. type: integer tls: @@ -627,6 +630,7 @@ spec: description: |- Syntax defines the router's rule syntax. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/routing/rules-and-priority/#rulesyntax + Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. enum: - v3 @@ -1061,6 +1065,7 @@ spec: description: |- AutoDetect specifies whether to let the `Content-Type` header, if it has not been set by the backend, be automatically set to a value derived from the contents of the response. + Deprecated: AutoDetect option is deprecated, Content-Type middleware is only meant to be used to enable the content-type detection, please remove any usage of this option. type: boolean type: object @@ -2213,8 +2218,9 @@ spec: description: |- IPWhiteList defines the IPWhiteList middleware configuration. This middleware accepts/refuses connections based on the client IP. - Deprecated: please use IPAllowList instead. More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/middlewares/ipwhitelist/ + + Deprecated: please use IPAllowList instead. properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of @@ -2371,6 +2377,7 @@ spec: rootCAsSecrets: description: |- RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string @@ -2525,6 +2532,7 @@ spec: rootCAsSecrets: description: |- RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string @@ -2660,6 +2668,7 @@ spec: description: |- PreferServerCipherSuites defines whether the server chooses a cipher suite among his own instead of among the client's. It is enabled automatically when minVersion or maxVersion is set. + Deprecated: https://github.com/golang/go/issues/45430 type: boolean sniStrict: diff --git a/integration/healthcheck_test.go b/integration/healthcheck_test.go index 6ab2ac618..08f009de2 100644 --- a/integration/healthcheck_test.go +++ b/integration/healthcheck_test.go @@ -19,6 +19,7 @@ import ( // HealthCheck test suites. type HealthCheckSuite struct { BaseSuite + whoami1IP string whoami2IP string whoami3IP string diff --git a/integration/helloworld/helloworld.pb.go b/integration/helloworld/helloworld.pb.go index d3f26398c..6c65d422b 100644 --- a/integration/helloworld/helloworld.pb.go +++ b/integration/helloworld/helloworld.pb.go @@ -187,7 +187,7 @@ func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { s.RegisterService(&_Greeter_serviceDesc, srv) } -func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _Greeter_SayHello_Handler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) { in := new(HelloRequest) if err := dec(in); err != nil { return nil, err @@ -199,13 +199,13 @@ func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(in Server: srv, FullMethod: "/helloworld.Greeter/SayHello", } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { + handler := func(ctx context.Context, req any) (any, error) { return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) } return interceptor(ctx, in, info, handler) } -func _Greeter_StreamExample_Handler(srv interface{}, stream grpc.ServerStream) error { +func _Greeter_StreamExample_Handler(srv any, stream grpc.ServerStream) error { m := new(StreamExampleRequest) if err := stream.RecvMsg(m); err != nil { return err diff --git a/integration/https_test.go b/integration/https_test.go index 68dd1ed27..68319fea1 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -877,40 +877,6 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion() require.NoError(s.T(), err) } -// modifyCertificateConfFileContent replaces the content of a HTTPS configuration file. -func (s *HTTPSSuite) modifyCertificateConfFileContent(certFileName, confFileName string) { - file, err := os.OpenFile("./"+confFileName, os.O_WRONLY, os.ModeExclusive) - require.NoError(s.T(), err) - defer func() { - file.Close() - }() - err = file.Truncate(0) - require.NoError(s.T(), err) - - // If certificate file is not provided, just truncate the configuration file - if len(certFileName) > 0 { - tlsConf := dynamic.Configuration{ - TLS: &dynamic.TLSConfiguration{ - Certificates: []*traefiktls.CertAndStores{ - { - Certificate: traefiktls.Certificate{ - CertFile: types.FileOrContent("fixtures/https/" + certFileName + ".cert"), - KeyFile: types.FileOrContent("fixtures/https/" + certFileName + ".key"), - }, - }, - }, - }, - } - - var confBuffer bytes.Buffer - err := toml.NewEncoder(&confBuffer).Encode(tlsConf) - require.NoError(s.T(), err) - - _, err = file.Write(confBuffer.Bytes()) - require.NoError(s.T(), err) - } -} - func (s *HTTPSSuite) TestEntryPointHttpsRedirectAndPathModification() { file := s.adaptFile("fixtures/https/https_redirect.toml", struct{}{}) s.traefikCmd(withConfigFile(file)) @@ -1177,6 +1143,40 @@ func (s *HTTPSSuite) TestWithInvalidTLSOption() { } } +// modifyCertificateConfFileContent replaces the content of a HTTPS configuration file. +func (s *HTTPSSuite) modifyCertificateConfFileContent(certFileName, confFileName string) { + file, err := os.OpenFile("./"+confFileName, os.O_WRONLY, os.ModeExclusive) + require.NoError(s.T(), err) + defer func() { + file.Close() + }() + err = file.Truncate(0) + require.NoError(s.T(), err) + + // If certificate file is not provided, just truncate the configuration file + if len(certFileName) > 0 { + tlsConf := dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{ + Certificates: []*traefiktls.CertAndStores{ + { + Certificate: traefiktls.Certificate{ + CertFile: types.FileOrContent("fixtures/https/" + certFileName + ".cert"), + KeyFile: types.FileOrContent("fixtures/https/" + certFileName + ".key"), + }, + }, + }, + }, + } + + var confBuffer bytes.Buffer + err := toml.NewEncoder(&confBuffer).Encode(tlsConf) + require.NoError(s.T(), err) + + _, err = file.Write(confBuffer.Bytes()) + require.NoError(s.T(), err) + } +} + func (s *SimpleSuite) TestMaxConcurrentStream() { file := s.adaptFile("fixtures/https/max_concurrent_stream.toml", struct{}{}) diff --git a/integration/integration_test.go b/integration/integration_test.go index 9ef4f8782..76ae5076d 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -71,45 +71,12 @@ type composeDeploy struct { type BaseSuite struct { suite.Suite + containers map[string]testcontainers.Container network *testcontainers.DockerNetwork hostIP string } -func (s *BaseSuite) waitForTraefik(containerName string) { - time.Sleep(1 * time.Second) - - // Wait for Traefik to turn ready. - req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/rawdata", nil) - require.NoError(s.T(), err) - - err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains(containerName)) - require.NoError(s.T(), err) -} - -func (s *BaseSuite) displayTraefikLogFile(path string) { - if s.T().Failed() { - if _, err := os.Stat(path); !os.IsNotExist(err) { - content, errRead := os.ReadFile(path) - // TODO TestName - // fmt.Printf("%s: Traefik logs: \n", c.TestName()) - fmt.Print("Traefik logs: \n") - if errRead == nil { - fmt.Println(string(content)) - } else { - fmt.Println(errRead) - } - } else { - // fmt.Printf("%s: No Traefik logs.\n", c.TestName()) - fmt.Print("No Traefik logs.\n") - } - errRemove := os.Remove(path) - if errRemove != nil { - fmt.Println(errRemove) - } - } -} - func (s *BaseSuite) SetupSuite() { if isDockerDesktop(s.T()) { _, err := os.Stat(tailscaleSecretFilePath) @@ -409,7 +376,7 @@ func (s *BaseSuite) displayTraefikLog(output *bytes.Buffer) { if output == nil || output.Len() == 0 { log.Info().Msg("No Traefik logs.") } else { - for _, line := range strings.Split(output.String(), "\n") { + for line := range strings.SplitSeq(output.String(), "\n") { log.Info().Msg(line) } } @@ -425,7 +392,7 @@ func (s *BaseSuite) getDockerHost() string { return dockerHost } -func (s *BaseSuite) adaptFile(path string, tempObjects interface{}) string { +func (s *BaseSuite) adaptFile(path string, tempObjects any) string { // Load file tmpl, err := template.ParseFiles(path) require.NoError(s.T(), err) @@ -513,3 +480,37 @@ func (s *BaseSuite) composeExec(service string, args ...string) string { return string(content) } + +func (s *BaseSuite) waitForTraefik(containerName string) { + time.Sleep(1 * time.Second) + + // Wait for Traefik to turn ready. + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/api/rawdata", nil) + require.NoError(s.T(), err) + + err = try.Request(req, 2*time.Second, try.StatusCodeIs(http.StatusOK), try.BodyContains(containerName)) + require.NoError(s.T(), err) +} + +func (s *BaseSuite) displayTraefikLogFile(path string) { + if s.T().Failed() { + if _, err := os.Stat(path); !os.IsNotExist(err) { + content, errRead := os.ReadFile(path) + // TODO TestName + // fmt.Printf("%s: Traefik logs: \n", c.TestName()) + fmt.Print("Traefik logs: \n") + if errRead == nil { + fmt.Println(string(content)) + } else { + fmt.Println(errRead) + } + } else { + // fmt.Printf("%s: No Traefik logs.\n", c.TestName()) + fmt.Print("No Traefik logs.\n") + } + errRemove := os.Remove(path) + if errRemove != nil { + fmt.Println(errRemove) + } + } +} diff --git a/integration/log_rotation_test.go b/integration/log_rotation_test.go index c6ee5574f..90310a608 100644 --- a/integration/log_rotation_test.go +++ b/integration/log_rotation_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package integration diff --git a/integration/proxy_protocol_test.go b/integration/proxy_protocol_test.go index f30a641c7..893daa718 100644 --- a/integration/proxy_protocol_test.go +++ b/integration/proxy_protocol_test.go @@ -3,6 +3,7 @@ package integration import ( "bufio" "net" + "strings" "testing" "time" @@ -15,6 +16,7 @@ import ( type ProxyProtocolSuite struct { BaseSuite + whoamiIP string } @@ -124,15 +126,16 @@ func proxyProtoRequest(address string, version byte) (string, error) { } // Read the response from the server - var content string + var content strings.Builder scanner := bufio.NewScanner(conn) for scanner.Scan() { - content += scanner.Text() + "\n" + content.WriteString(scanner.Text()) + content.WriteString("\n") } if scanner.Err() != nil { return "", err } - return content, nil + return content.String(), nil } diff --git a/integration/ratelimit_test.go b/integration/ratelimit_test.go index c0f0c96b3..3bf8092a6 100644 --- a/integration/ratelimit_test.go +++ b/integration/ratelimit_test.go @@ -13,6 +13,7 @@ import ( type RateLimitSuite struct { BaseSuite + ServerIP string RedisEndpoint string } diff --git a/integration/redis_sentinel_test.go b/integration/redis_sentinel_test.go index 542a63728..4891c2a42 100644 --- a/integration/redis_sentinel_test.go +++ b/integration/redis_sentinel_test.go @@ -28,6 +28,7 @@ import ( // Redis test suites. type RedisSentinelSuite struct { BaseSuite + kvClient store.Store redisEndpoints []string } @@ -75,36 +76,6 @@ func (s *RedisSentinelSuite) TearDownSuite() { } } -func (s *RedisSentinelSuite) setupSentinelConfiguration(ports []string) { - for i, port := range ports { - templateValue := struct{ SentinelPort string }{SentinelPort: port} - - // Load file - templateFile := "resources/compose/config/sentinel_template.conf" - tmpl, err := template.ParseFiles(templateFile) - require.NoError(s.T(), err) - - folder, prefix := filepath.Split(templateFile) - - fileName := fmt.Sprintf("%s/sentinel%d.conf", folder, i+1) - tmpFile, err := os.Create(fileName) - require.NoError(s.T(), err) - defer tmpFile.Close() - - err = tmpFile.Chmod(0o666) - require.NoError(s.T(), err) - - model := structs.Map(templateValue) - model["SelfFilename"] = tmpFile.Name() - - err = tmpl.ExecuteTemplate(tmpFile, prefix, model) - require.NoError(s.T(), err) - - err = tmpFile.Sync() - require.NoError(s.T(), err) - } -} - func (s *RedisSentinelSuite) TestSentinelConfiguration() { file := s.adaptFile("fixtures/redis/sentinel.toml", struct{ RedisAddress string }{ RedisAddress: strings.Join(s.redisEndpoints, `","`), @@ -201,3 +172,33 @@ func (s *RedisSentinelSuite) TestSentinelConfiguration() { log.Info().Msg(text) } } + +func (s *RedisSentinelSuite) setupSentinelConfiguration(ports []string) { + for i, port := range ports { + templateValue := struct{ SentinelPort string }{SentinelPort: port} + + // Load file + templateFile := "resources/compose/config/sentinel_template.conf" + tmpl, err := template.ParseFiles(templateFile) + require.NoError(s.T(), err) + + folder, prefix := filepath.Split(templateFile) + + fileName := fmt.Sprintf("%s/sentinel%d.conf", folder, i+1) + tmpFile, err := os.Create(fileName) + require.NoError(s.T(), err) + defer tmpFile.Close() + + err = tmpFile.Chmod(0o666) + require.NoError(s.T(), err) + + model := structs.Map(templateValue) + model["SelfFilename"] = tmpFile.Name() + + err = tmpl.ExecuteTemplate(tmpFile, prefix, model) + require.NoError(s.T(), err) + + err = tmpFile.Sync() + require.NoError(s.T(), err) + } +} diff --git a/integration/redis_test.go b/integration/redis_test.go index e1ed10899..cf6cdf2c4 100644 --- a/integration/redis_test.go +++ b/integration/redis_test.go @@ -24,6 +24,7 @@ import ( // Redis test suites. type RedisSuite struct { BaseSuite + kvClient store.Store redisEndpoints []string } diff --git a/integration/rest_test.go b/integration/rest_test.go index 2600a08c9..467d9f118 100644 --- a/integration/rest_test.go +++ b/integration/rest_test.go @@ -18,6 +18,7 @@ import ( type RestSuite struct { BaseSuite + whoamiAddr string } diff --git a/integration/retry_test.go b/integration/retry_test.go index 12d0be8ac..fbd510833 100644 --- a/integration/retry_test.go +++ b/integration/retry_test.go @@ -15,6 +15,7 @@ import ( type RetrySuite struct { BaseSuite + whoamiIP string } diff --git a/integration/tcp_healthcheck_test.go b/integration/tcp_healthcheck_test.go index d79fb610b..13727b7b2 100644 --- a/integration/tcp_healthcheck_test.go +++ b/integration/tcp_healthcheck_test.go @@ -15,6 +15,7 @@ import ( // TCPHealthCheckSuite test suite for TCP health checks. type TCPHealthCheckSuite struct { BaseSuite + whoamitcp1IP string whoamitcp2IP string } diff --git a/integration/tracing_test.go b/integration/tracing_test.go index 71e0b1b36..d35d7ad62 100644 --- a/integration/tracing_test.go +++ b/integration/tracing_test.go @@ -21,6 +21,7 @@ import ( type TracingSuite struct { BaseSuite + whoamiIP string whoamiPort int tempoIP string diff --git a/integration/try/try.go b/integration/try/try.go index df3db0c9e..5a432a13d 100644 --- a/integration/try/try.go +++ b/integration/try/try.go @@ -19,6 +19,7 @@ const ( type timedAction func(timeout time.Duration, operation DoCondition) error // Sleep pauses the current goroutine for at least the duration d. +// // Deprecated: Use only when use another Try[...] functions is not possible. func Sleep(d time.Duration) { d = applyCIMultiplier(d) @@ -92,10 +93,7 @@ func Do(timeout time.Duration, operation DoCondition) error { panic("timeout must be larger than zero") } - interval := time.Duration(math.Ceil(float64(timeout) / 15.0)) - if interval > maxInterval { - interval = maxInterval - } + interval := min(time.Duration(math.Ceil(float64(timeout)/15.0)), maxInterval) timeout = applyCIMultiplier(timeout) diff --git a/integration/zk_test.go b/integration/zk_test.go index 8955c530e..ecee25275 100644 --- a/integration/zk_test.go +++ b/integration/zk_test.go @@ -24,6 +24,7 @@ import ( // Zk test suites. type ZookeeperSuite struct { BaseSuite + kvClient store.Store zookeeperAddr string } diff --git a/internal/gendoc.go b/internal/gendoc.go index 34c9e0002..df8098b0c 100644 --- a/internal/gendoc.go +++ b/internal/gendoc.go @@ -196,7 +196,7 @@ type errWriter struct { err error } -func (ew *errWriter) writeln(a ...interface{}) { +func (ew *errWriter) writeln(a ...any) { if ew.err != nil { return } diff --git a/pkg/api/criterion.go b/pkg/api/criterion.go index ba04c784e..f40b76aee 100644 --- a/pkg/api/criterion.go +++ b/pkg/api/criterion.go @@ -84,13 +84,7 @@ func (c *searchCriterion) filterMiddleware(mns []string) bool { return true } - for _, mn := range mns { - if c.MiddlewareName == mn { - return true - } - } - - return false + return slices.Contains(mns, c.MiddlewareName) } func pagination(request *http.Request, maximum int) (pageInfo, error) { @@ -109,10 +103,7 @@ func pagination(request *http.Request, maximum int) (pageInfo, error) { return pageInfo{}, fmt.Errorf("invalid request: page: %d, per_page: %d", page, perPage) } - endIndex := startIndex + perPage - if endIndex >= maximum { - endIndex = maximum - } + endIndex := min(startIndex+perPage, maximum) nextPage := 1 if page*perPage < maximum { diff --git a/pkg/api/debug.go b/pkg/api/debug.go index b207043ef..f5fdfb74c 100644 --- a/pkg/api/debug.go +++ b/pkg/api/debug.go @@ -15,7 +15,7 @@ func init() { expvar.Publish("Goroutines2", expvar.Func(goroutines)) } -func goroutines() interface{} { +func goroutines() any { return runtime.NumGoroutine() } diff --git a/pkg/api/handler.go b/pkg/api/handler.go index 0165da9e8..2cd9e2270 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -30,11 +30,13 @@ func writeError(rw http.ResponseWriter, msg string, code int) { type serviceInfoRepresentation struct { *runtime.ServiceInfo + ServerStatus map[string]string `json:"serverStatus,omitempty"` } type tcpServiceInfoRepresentation struct { *runtime.TCPServiceInfo + ServerStatus map[string]string `json:"serverStatus,omitempty"` } @@ -164,7 +166,7 @@ func getProviderName(id string) string { return strings.SplitN(id, "@", 2)[1] } -func extractType(element interface{}) string { +func extractType(element any) string { v := reflect.ValueOf(element).Elem() for i := range v.NumField() { field := v.Field(i) diff --git a/pkg/api/handler_entrypoint.go b/pkg/api/handler_entrypoint.go index 90dc66daa..2595d9faa 100644 --- a/pkg/api/handler_entrypoint.go +++ b/pkg/api/handler_entrypoint.go @@ -15,6 +15,7 @@ import ( type entryPointRepresentation struct { *static.EntryPoint + Name string `json:"name,omitempty"` } diff --git a/pkg/api/handler_entrypoint_test.go b/pkg/api/handler_entrypoint_test.go index 768cd3dcd..c4c2dccf8 100644 --- a/pkg/api/handler_entrypoint_test.go +++ b/pkg/api/handler_entrypoint_test.go @@ -235,7 +235,7 @@ func TestHandler_EntryPoints(t *testing.T) { require.NoError(t, err) if *updateExpected { - var results interface{} + var results any err := json.Unmarshal(contents, &results) require.NoError(t, err) diff --git a/pkg/api/handler_http.go b/pkg/api/handler_http.go index a0ca84ce7..24df129f1 100644 --- a/pkg/api/handler_http.go +++ b/pkg/api/handler_http.go @@ -16,6 +16,7 @@ import ( type routerRepresentation struct { *runtime.RouterInfo + Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` } @@ -34,6 +35,7 @@ func newRouterRepresentation(name string, rt *runtime.RouterInfo) routerRepresen type serviceRepresentation struct { *runtime.ServiceInfo + Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` Type string `json:"type,omitempty"` @@ -52,6 +54,7 @@ func newServiceRepresentation(name string, si *runtime.ServiceInfo) serviceRepre type middlewareRepresentation struct { *runtime.MiddlewareInfo + Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` Type string `json:"type,omitempty"` diff --git a/pkg/api/handler_http_test.go b/pkg/api/handler_http_test.go index e34b1bfe2..4f736eed7 100644 --- a/pkg/api/handler_http_test.go +++ b/pkg/api/handler_http_test.go @@ -1028,7 +1028,7 @@ func TestHandler_HTTP(t *testing.T) { require.NoError(t, err) if *updateExpected { - var results interface{} + var results any err := json.Unmarshal(contents, &results) require.NoError(t, err) diff --git a/pkg/api/handler_overview_test.go b/pkg/api/handler_overview_test.go index 7b07106b9..8dabef1b1 100644 --- a/pkg/api/handler_overview_test.go +++ b/pkg/api/handler_overview_test.go @@ -239,7 +239,7 @@ func TestHandler_Overview(t *testing.T) { KubernetesCRD: &crd.Provider{}, Rest: &rest.Provider{}, Plugin: map[string]static.PluginConf{ - "test": map[string]interface{}{}, + "test": map[string]any{}, }, }, }, @@ -292,7 +292,7 @@ func TestHandler_Overview(t *testing.T) { require.NoError(t, err) if *updateExpected { - var results interface{} + var results any err := json.Unmarshal(contents, &results) require.NoError(t, err) diff --git a/pkg/api/handler_tcp.go b/pkg/api/handler_tcp.go index cc8f9aeed..470b1ecb6 100644 --- a/pkg/api/handler_tcp.go +++ b/pkg/api/handler_tcp.go @@ -15,6 +15,7 @@ import ( type tcpRouterRepresentation struct { *runtime.TCPRouterInfo + Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` } @@ -29,6 +30,7 @@ func newTCPRouterRepresentation(name string, rt *runtime.TCPRouterInfo) tcpRoute type tcpServiceRepresentation struct { *runtime.TCPServiceInfo + Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` Type string `json:"type,omitempty"` @@ -47,6 +49,7 @@ func newTCPServiceRepresentation(name string, si *runtime.TCPServiceInfo) tcpSer type tcpMiddlewareRepresentation struct { *runtime.TCPMiddlewareInfo + Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` Type string `json:"type,omitempty"` diff --git a/pkg/api/handler_tcp_test.go b/pkg/api/handler_tcp_test.go index b0ee4cc2f..1cf829260 100644 --- a/pkg/api/handler_tcp_test.go +++ b/pkg/api/handler_tcp_test.go @@ -964,7 +964,7 @@ func TestHandler_TCP(t *testing.T) { require.NoError(t, err) if *updateExpected { - var results interface{} + var results any err := json.Unmarshal(contents, &results) require.NoError(t, err) diff --git a/pkg/api/handler_test.go b/pkg/api/handler_test.go index 3eb7f8aa0..3adfffe06 100644 --- a/pkg/api/handler_test.go +++ b/pkg/api/handler_test.go @@ -178,7 +178,7 @@ func TestHandler_GetMiddleware(t *testing.T) { middlewareName string conf runtime.Configuration expectedStatus int - expected interface{} + expected any }{ { desc: "Middleware not found", diff --git a/pkg/api/handler_udp.go b/pkg/api/handler_udp.go index 60f4b7178..db7185abd 100644 --- a/pkg/api/handler_udp.go +++ b/pkg/api/handler_udp.go @@ -15,6 +15,7 @@ import ( type udpRouterRepresentation struct { *runtime.UDPRouterInfo + Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` } @@ -29,6 +30,7 @@ func newUDPRouterRepresentation(name string, rt *runtime.UDPRouterInfo) udpRoute type udpServiceRepresentation struct { *runtime.UDPServiceInfo + Name string `json:"name,omitempty"` Provider string `json:"provider,omitempty"` Type string `json:"type,omitempty"` diff --git a/pkg/api/handler_udp_test.go b/pkg/api/handler_udp_test.go index bd8625dfe..547c328b0 100644 --- a/pkg/api/handler_udp_test.go +++ b/pkg/api/handler_udp_test.go @@ -594,7 +594,7 @@ func TestHandler_UDP(t *testing.T) { require.NoError(t, err) if *updateExpected { - var results interface{} + var results any err := json.Unmarshal(contents, &results) require.NoError(t, err) diff --git a/pkg/cli/deprecation.go b/pkg/cli/deprecation.go index 337434aed..233506c8e 100644 --- a/pkg/cli/deprecation.go +++ b/pkg/cli/deprecation.go @@ -82,7 +82,7 @@ func logDeprecations(arguments []string) (bool, error) { if filePath != "" { // We don't rely on the Parser file loader here to avoid issues with unknown fields. // Parse file content into a generic map. - var fileConfig map[string]interface{} + var fileConfig map[string]any if err := file.Decode(filePath, &fileConfig); err != nil { return false, fmt.Errorf("decoding configuration file %s: %w", filePath, err) } @@ -106,7 +106,7 @@ func logDeprecations(arguments []string) (bool, error) { if len(vars) > 0 { // We don't rely on the Parser env loader here to avoid issues with unknown fields. // Decode environment variables to a generic map. - var envConfig map[string]interface{} + var envConfig map[string]any if err := env.Decode(vars, env.DefaultNamePrefix, &envConfig); err != nil { return false, fmt.Errorf("decoding environment variables: %w", err) } @@ -130,9 +130,9 @@ func logDeprecations(arguments []string) (bool, error) { // flattenToLabels recursively flattens a nested map into label key-value pairs. // Example: {"experimental": {"http3": true}} -> {"traefik.experimental.http3": "true"}. -func flattenToLabels(config interface{}, currKey string, labels map[string]string) { +func flattenToLabels(config any, currKey string, labels map[string]string) { switch v := config.(type) { - case map[string]interface{}: + case map[string]any: for key, value := range v { newKey := key if currKey != "" { @@ -140,7 +140,7 @@ func flattenToLabels(config interface{}, currKey string, labels map[string]strin } flattenToLabels(value, newKey, labels) } - case []interface{}: + case []any: for i, item := range v { newKey := currKey + "[" + strconv.Itoa(i) + "]" flattenToLabels(item, newKey, labels) @@ -168,7 +168,7 @@ func parseDeprecatedConfig(labels map[string]string) (*configuration, error) { // Filter unknown nodes and check for deprecated options. config := &configuration{} - filterUnknownNodes(reflect.TypeOf(config), node) + filterUnknownNodes(reflect.TypeFor[*configuration](), node) // If no config remains we can return without error, to allow other loaders to proceed. if node == nil || len(node.Children) == 0 { diff --git a/pkg/cli/loader_file.go b/pkg/cli/loader_file.go index f5f11fa51..b33768b6a 100644 --- a/pkg/cli/loader_file.go +++ b/pkg/cli/loader_file.go @@ -62,7 +62,7 @@ func (f *FileLoader) Load(args []string, cmd *cli.Command) (bool, error) { // loadConfigFiles tries to decode the given configuration file and all default locations for the configuration file. // It stops as soon as decoding one of them is successful. -func loadConfigFiles(configFile string, element interface{}) (string, error) { +func loadConfigFiles(configFile string, element any) (string, error) { finder := cli.Finder{ BasePaths: []string{"/etc/traefik/traefik", "$XDG_CONFIG_HOME/traefik", "$HOME/.config/traefik", "./traefik"}, Extensions: []string{"toml", "yaml", "yml"}, diff --git a/pkg/collector/hydratation/hydration.go b/pkg/collector/hydratation/hydration.go index 74009817a..eff17ae58 100644 --- a/pkg/collector/hydratation/hydration.go +++ b/pkg/collector/hydratation/hydration.go @@ -18,7 +18,7 @@ const ( ) // Hydrate hydrates a configuration. -func Hydrate(element interface{}) error { +func Hydrate(element any) error { field := reflect.ValueOf(element) return fill(field) } @@ -81,7 +81,7 @@ func fill(field reflect.Value) error { return nil } -func setTyped(field reflect.Value, i interface{}) { +func setTyped(field reflect.Value, i any) { baseValue := reflect.ValueOf(i) if field.Kind().String() == field.Type().String() { field.Set(baseValue) diff --git a/pkg/config/dynamic/config_test.go b/pkg/config/dynamic/config_test.go index 4a04509c4..368a211da 100644 --- a/pkg/config/dynamic/config_test.go +++ b/pkg/config/dynamic/config_test.go @@ -21,7 +21,7 @@ func TestDeepCopy(t *testing.T) { cfgDeepCopy := cfg.DeepCopy() assert.NotEqual(t, reflect.ValueOf(cfgDeepCopy), reflect.ValueOf(cfg)) - assert.Equal(t, reflect.TypeOf(cfgDeepCopy), reflect.TypeOf(cfg)) + assert.Equal(t, reflect.TypeOf(cfgDeepCopy), reflect.TypeOf(cfg)) //nolint:modernize // Comparing runtime types of two values. assert.Equal(t, cfgDeepCopy, cfg) // Update cfg @@ -32,6 +32,6 @@ func TestDeepCopy(t *testing.T) { assert.Equal(t, cfgCopy, cfg) assert.NotEqual(t, reflect.ValueOf(cfgDeepCopy), reflect.ValueOf(cfg)) - assert.Equal(t, reflect.TypeOf(cfgDeepCopy), reflect.TypeOf(cfg)) + assert.Equal(t, reflect.TypeOf(cfgDeepCopy), reflect.TypeOf(cfg)) //nolint:modernize // Comparing runtime types of two values. assert.NotEqual(t, cfgDeepCopy, cfg) } diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index fa10c302b..5b2157d9b 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -69,6 +69,7 @@ type GrpcWeb struct { type ContentType struct { // AutoDetect specifies whether to let the `Content-Type` header, if it has not been set by the backend, // be automatically set to a value derived from the contents of the response. + // // Deprecated: AutoDetect option is deprecated, Content-Type middleware is only meant to be used to enable the content-type detection, please remove any usage of this option. AutoDetect *bool `json:"autoDetect,omitempty" toml:"autoDetect,omitempty" yaml:"autoDetect,omitempty" export:"true"` } @@ -481,6 +482,7 @@ func (s *IPStrategy) Get() (ip.Strategy, error) { // IPWhiteList holds the IP whitelist middleware configuration. // This middleware limits allowed requests based on the client IP. // More info: https://doc.traefik.io/traefik/v3.6/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). Required. diff --git a/pkg/config/dynamic/plugins_test.go b/pkg/config/dynamic/plugins_test.go index 6021362dd..83b05dcc6 100644 --- a/pkg/config/dynamic/plugins_test.go +++ b/pkg/config/dynamic/plugins_test.go @@ -44,11 +44,11 @@ func TestPluginConf_DeepCopy_mapOfStruct(t *testing.T) { } func TestPluginConf_DeepCopy_map(t *testing.T) { - m := map[string]interface{}{ + m := map[string]any{ "name": "bar", } p := PluginConf{ - "config": map[string]interface{}{ + "config": map[string]any{ "foo": m, }, } @@ -64,7 +64,7 @@ func TestPluginConf_DeepCopy_map(t *testing.T) { func TestPluginConf_DeepCopy_panic(t *testing.T) { p := &PluginConf{ - "config": map[string]interface{}{ + "config": map[string]any{ "foo": &Foo{Name: "gigi"}, }, } diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go index a68be77b9..51a96373a 100644 --- a/pkg/config/dynamic/tcp_config.go +++ b/pkg/config/dynamic/tcp_config.go @@ -88,6 +88,7 @@ type TCPServersLoadBalancer struct { Servers []TCPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server" export:"true"` ServersTransport string `json:"serversTransport,omitempty" toml:"serversTransport,omitempty" yaml:"serversTransport,omitempty" export:"true"` // ProxyProtocol holds the PROXY Protocol configuration. + // // Deprecated: use ServersTransport to configure ProxyProtocol instead. ProxyProtocol *ProxyProtocol `json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` // TerminationDelay, corresponds to the deadline that the proxy sets, after one @@ -95,6 +96,7 @@ type TCPServersLoadBalancer struct { // connection, to close the reading capability as well, hence fully terminating the // connection. It is a duration in milliseconds, defaulting to 100. A negative value // means an infinite deadline (i.e. the reading capability is never closed). + // // Deprecated: use ServersTransport to configure the TerminationDelay instead. TerminationDelay *int `json:"terminationDelay,omitempty" toml:"terminationDelay,omitempty" yaml:"terminationDelay,omitempty" export:"true"` HealthCheck *TCPServerHealthCheck `json:"healthCheck,omitempty" toml:"healthCheck,omitempty" yaml:"healthCheck,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` diff --git a/pkg/config/dynamic/tcp_middlewares.go b/pkg/config/dynamic/tcp_middlewares.go index 1f20e8ca2..cdc2163a0 100644 --- a/pkg/config/dynamic/tcp_middlewares.go +++ b/pkg/config/dynamic/tcp_middlewares.go @@ -26,6 +26,7 @@ type TCPInFlightConn struct { // +k8s:deepcopy-gen=true // TCPIPWhiteList holds the TCP IPWhiteList middleware configuration. +// // Deprecated: please use IPAllowList instead. type TCPIPWhiteList struct { // SourceRange defines the allowed IPs (or ranges of allowed IPs by using CIDR notation). diff --git a/pkg/config/kv/kv.go b/pkg/config/kv/kv.go index ad467b00d..eb71dee0c 100644 --- a/pkg/config/kv/kv.go +++ b/pkg/config/kv/kv.go @@ -13,7 +13,7 @@ import ( // KV pairs -> tree of untyped nodes // untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // "typed" nodes -> typed element. -func Decode(pairs []*store.KVPair, element interface{}, rootName string) error { +func Decode(pairs []*store.KVPair, element any, rootName string) error { if element == nil { return nil } @@ -34,7 +34,7 @@ func Decode(pairs []*store.KVPair, element interface{}, rootName string) error { return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: false}) } -func getRootFieldNames(rootName string, element interface{}) []string { +func getRootFieldNames(rootName string, element any) []string { if element == nil { return nil } diff --git a/pkg/config/label/label.go b/pkg/config/label/label.go index ed1e71ddf..3084778fd 100644 --- a/pkg/config/label/label.go +++ b/pkg/config/label/label.go @@ -31,6 +31,6 @@ func EncodeConfiguration(conf *dynamic.Configuration) (map[string]string, error) // Decode converts the labels to an element. // labels -> [ node -> node + metadata (type) ] -> element (node). -func Decode(labels map[string]string, element interface{}, filters ...string) error { +func Decode(labels map[string]string, element any, filters ...string) error { return parser.Decode(labels, element, parser.DefaultRootName, filters...) } diff --git a/pkg/config/runtime/runtime_http.go b/pkg/config/runtime/runtime_http.go index f8f33fcec..18a1cd850 100644 --- a/pkg/config/runtime/runtime_http.go +++ b/pkg/config/runtime/runtime_http.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "maps" "slices" "sort" "sync" @@ -74,6 +75,7 @@ func unique(src []string) []string { // RouterInfo holds information about a currently running HTTP router. type RouterInfo struct { *dynamic.Router // dynamic configuration + // Err contains all the errors that occurred during router's creation. Err []string `json:"error,omitempty"` // Status reports whether the router is disabled, in a warning state, or all good (enabled). @@ -91,10 +93,8 @@ type RouterInfo struct { // AddError adds err to r.Err, if it does not already exist. // If critical is set, r is marked as disabled. func (r *RouterInfo) AddError(err error, critical bool) { - for _, value := range r.Err { - if value == err.Error() { - return - } + if slices.Contains(r.Err, err.Error()) { + return } r.Err = append(r.Err, err.Error()) @@ -112,6 +112,7 @@ func (r *RouterInfo) AddError(err error, critical bool) { // MiddlewareInfo holds information about a currently running middleware. type MiddlewareInfo struct { *dynamic.Middleware // dynamic configuration + // Err contains all the errors that occurred during service creation. Err []string `json:"error,omitempty"` Status string `json:"status,omitempty"` @@ -121,10 +122,8 @@ type MiddlewareInfo struct { // AddError adds err to s.Err, if it does not already exist. // If critical is set, m is marked as disabled. func (m *MiddlewareInfo) AddError(err error, critical bool) { - for _, value := range m.Err { - if value == err.Error() { - return - } + if slices.Contains(m.Err, err.Error()) { + return } m.Err = append(m.Err, err.Error()) @@ -142,6 +141,7 @@ func (m *MiddlewareInfo) AddError(err error, critical bool) { // ServiceInfo holds information about a currently running service. type ServiceInfo struct { *dynamic.Service // dynamic configuration + // Err contains all the errors that occurred during service creation. Err []string `json:"error,omitempty"` // Status reports whether the service is disabled, in a warning state, or all good (enabled). @@ -157,10 +157,8 @@ type ServiceInfo struct { // AddError adds err to s.Err, if it does not already exist. // If critical is set, s is marked as disabled. func (s *ServiceInfo) AddError(err error, critical bool) { - for _, value := range s.Err { - if value == err.Error() { - return - } + if slices.Contains(s.Err, err.Error()) { + return } s.Err = append(s.Err, err.Error()) @@ -197,9 +195,5 @@ func (s *ServiceInfo) GetAllStatus() map[string]string { return nil } - allStatus := make(map[string]string, len(s.serverStatus)) - for k, v := range s.serverStatus { - allStatus[k] = v - } - return allStatus + return maps.Clone(s.serverStatus) } diff --git a/pkg/config/runtime/runtime_tcp.go b/pkg/config/runtime/runtime_tcp.go index 07473b51e..90b81fce7 100644 --- a/pkg/config/runtime/runtime_tcp.go +++ b/pkg/config/runtime/runtime_tcp.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "maps" "slices" "sync" @@ -49,8 +50,9 @@ func (c *Configuration) GetTCPRoutersByEntryPoints(ctx context.Context, entryPoi // TCPRouterInfo holds information about a currently running TCP router. type TCPRouterInfo struct { - *dynamic.TCPRouter // dynamic configuration - Err []string `json:"error,omitempty"` // initialization error + *dynamic.TCPRouter // dynamic configuration + + Err []string `json:"error,omitempty"` // initialization error // Status reports whether the router is disabled, in a warning state, or all good (enabled). // If not in "enabled" state, the reason for it should be in the list of Err. // It is the caller's responsibility to set the initial status. @@ -61,10 +63,8 @@ type TCPRouterInfo struct { // AddError adds err to r.Err, if it does not already exist. // If critical is set, r is marked as disabled. func (r *TCPRouterInfo) AddError(err error, critical bool) { - for _, value := range r.Err { - if value == err.Error() { - return - } + if slices.Contains(r.Err, err.Error()) { + return } r.Err = append(r.Err, err.Error()) @@ -81,8 +81,9 @@ func (r *TCPRouterInfo) AddError(err error, critical bool) { // TCPServiceInfo holds information about a currently running TCP service. type TCPServiceInfo struct { - *dynamic.TCPService // dynamic configuration - Err []string `json:"error,omitempty"` // initialization error + *dynamic.TCPService // dynamic configuration + + Err []string `json:"error,omitempty"` // initialization error // Status reports whether the service is disabled, in a warning state, or all good (enabled). // If not in "enabled" state, the reason for it should be in the list of Err. // It is the caller's responsibility to set the initial status. @@ -96,10 +97,8 @@ type TCPServiceInfo struct { // AddError adds err to s.Err, if it does not already exist. // If critical is set, s is marked as disabled. func (s *TCPServiceInfo) AddError(err error, critical bool) { - for _, value := range s.Err { - if value == err.Error() { - return - } + if slices.Contains(s.Err, err.Error()) { + return } s.Err = append(s.Err, err.Error()) @@ -135,15 +134,14 @@ func (s *TCPServiceInfo) GetAllStatus() map[string]string { } allStatus := make(map[string]string, len(s.serverStatus)) - for k, v := range s.serverStatus { - allStatus[k] = v - } + maps.Copy(allStatus, s.serverStatus) return allStatus } // TCPMiddlewareInfo holds information about a currently running middleware. type TCPMiddlewareInfo struct { *dynamic.TCPMiddleware // dynamic configuration + // Err contains all the errors that occurred during service creation. Err []string `json:"error,omitempty"` Status string `json:"status,omitempty"` @@ -153,10 +151,8 @@ type TCPMiddlewareInfo struct { // AddError adds err to s.Err, if it does not already exist. // If critical is set, m is marked as disabled. func (m *TCPMiddlewareInfo) AddError(err error, critical bool) { - for _, value := range m.Err { - if value == err.Error() { - return - } + if slices.Contains(m.Err, err.Error()) { + return } m.Err = append(m.Err, err.Error()) diff --git a/pkg/config/runtime/runtime_udp.go b/pkg/config/runtime/runtime_udp.go index 912f986f1..fd82e0495 100644 --- a/pkg/config/runtime/runtime_udp.go +++ b/pkg/config/runtime/runtime_udp.go @@ -54,8 +54,9 @@ func (c *Configuration) GetUDPRoutersByEntryPoints(ctx context.Context, entryPoi // UDPRouterInfo holds information about a currently running UDP router. type UDPRouterInfo struct { - *dynamic.UDPRouter // dynamic configuration - Err []string `json:"error,omitempty"` // initialization error + *dynamic.UDPRouter // dynamic configuration + + Err []string `json:"error,omitempty"` // initialization error // Status reports whether the router is disabled, in a warning state, or all good (enabled). // If not in "enabled" state, the reason for it should be in the list of Err. // It is the caller's responsibility to set the initial status. @@ -66,10 +67,8 @@ type UDPRouterInfo struct { // AddError adds err to r.Err, if it does not already exist. // If critical is set, r is marked as disabled. func (r *UDPRouterInfo) AddError(err error, critical bool) { - for _, value := range r.Err { - if value == err.Error() { - return - } + if slices.Contains(r.Err, err.Error()) { + return } r.Err = append(r.Err, err.Error()) @@ -86,8 +85,9 @@ func (r *UDPRouterInfo) AddError(err error, critical bool) { // UDPServiceInfo holds information about a currently running UDP service. type UDPServiceInfo struct { - *dynamic.UDPService // dynamic configuration - Err []string `json:"error,omitempty"` // initialization error + *dynamic.UDPService // dynamic configuration + + Err []string `json:"error,omitempty"` // initialization error // Status reports whether the service is disabled, in a warning state, or all good (enabled). // If not in "enabled" state, the reason for it should be in the list of Err. // It is the caller's responsibility to set the initial status. @@ -98,10 +98,8 @@ type UDPServiceInfo struct { // AddError adds err to s.Err, if it does not already exist. // If critical is set, s is marked as disabled. func (s *UDPServiceInfo) AddError(err error, critical bool) { - for _, value := range s.Err { - if value == err.Error() { - return - } + if slices.Contains(s.Err, err.Error()) { + return } s.Err = append(s.Err, err.Error()) diff --git a/pkg/config/static/plugins.go b/pkg/config/static/plugins.go index 5b2577e4e..ddf2f9cc1 100644 --- a/pkg/config/static/plugins.go +++ b/pkg/config/static/plugins.go @@ -1,4 +1,4 @@ package static // PluginConf holds the plugin configuration. -type PluginConf map[string]interface{} +type PluginConf map[string]any diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 9311b9405..e921e8a5e 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -393,21 +393,6 @@ func (c *Configuration) SetEffectiveConfiguration() { c.initACMEProvider() } -func (c *Configuration) hasUserDefinedEntrypoint() bool { - return len(c.EntryPoints) != 0 -} - -func (c *Configuration) initACMEProvider() { - for _, resolver := range c.CertificatesResolvers { - if resolver.ACME != nil { - resolver.ACME.CAServer = getSafeACMECAServer(resolver.ACME.CAServer) - } - } - - logger := logs.NoLevel(log.Logger, zerolog.DebugLevel).With().Str("lib", "lego").Logger() - legolog.Logger = logs.NewLogrusWrapper(logger) -} - // ValidateConfiguration validate that configuration is coherent. func (c *Configuration) ValidateConfiguration() error { for name, resolver := range c.CertificatesResolvers { @@ -494,6 +479,21 @@ func (c *Configuration) ValidateConfiguration() error { return nil } +func (c *Configuration) hasUserDefinedEntrypoint() bool { + return len(c.EntryPoints) != 0 +} + +func (c *Configuration) initACMEProvider() { + for _, resolver := range c.CertificatesResolvers { + if resolver.ACME != nil { + resolver.ACME.CAServer = getSafeACMECAServer(resolver.ACME.CAServer) + } + } + + logger := logs.NoLevel(log.Logger, zerolog.DebugLevel).With().Str("lib", "lego").Logger() + legolog.Logger = logs.NewLogrusWrapper(logger) +} + func getSafeACMECAServer(caServerSrc string) string { if len(caServerSrc) == 0 { return DefaultAcmeCAServer diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index e1be3343e..a0d1a833a 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -391,7 +391,7 @@ func (p *PassiveServiceHealthChecker) WrapHandler(ctx context.Context, next http } // We need to guarantee that only one goroutine (request) will update the status and create a timer for the target. - _, _, _ = p.timersGroup.Do(targetURL, func() (interface{}, error) { + _, _, _ = p.timersGroup.Do(targetURL, func() (any, error) { // A timer is already running for this target; // it means that the target is already considered unhealthy. if _, ok := p.timers.Load(targetURL); ok { diff --git a/pkg/healthcheck/mock_test.go b/pkg/healthcheck/mock_test.go index 483a5385d..0adcf1b25 100644 --- a/pkg/healthcheck/mock_test.go +++ b/pkg/healthcheck/mock_test.go @@ -165,6 +165,7 @@ type testLoadBalancer struct { // RWMutex needed due to parallel test execution: Both the system-under-test // and the test assertions reference the counters. *sync.RWMutex + numRemovedServers int numUpsertedServers int } diff --git a/pkg/job/job.go b/pkg/job/job.go index 752f7b905..c16146fca 100644 --- a/pkg/job/job.go +++ b/pkg/job/job.go @@ -17,6 +17,7 @@ const ( // If operation() takes more than MinJobInterval, Reset() is called in NextBackOff(). type BackOff struct { *backoff.ExponentialBackOff + MinJobInterval time.Duration } diff --git a/pkg/middlewares/accesslog/field_middleware_test.go b/pkg/middlewares/accesslog/field_middleware_test.go index 50a8a3406..b39167a07 100644 --- a/pkg/middlewares/accesslog/field_middleware_test.go +++ b/pkg/middlewares/accesslog/field_middleware_test.go @@ -12,7 +12,7 @@ import ( func TestConcatFieldHandler_ServeHTTP(t *testing.T) { testCases := []struct { desc string - existingValue interface{} + existingValue any newValue string expectedResult string }{ diff --git a/pkg/middlewares/accesslog/logdata.go b/pkg/middlewares/accesslog/logdata.go index 19dc43d6c..451588b8a 100644 --- a/pkg/middlewares/accesslog/logdata.go +++ b/pkg/middlewares/accesslog/logdata.go @@ -129,7 +129,7 @@ func init() { } // CoreLogData holds the fields computed from the request/response. -type CoreLogData map[string]interface{} +type CoreLogData map[string]any // LogData is the data captured by the middleware so that it can be logged. type LogData struct { diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index 5f21afc09..9ab1e6bd5 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -74,20 +74,6 @@ type Handler struct { wg sync.WaitGroup } -// AliceConstructor returns an alice.Constructor that wraps the Handler (conditionally) in a middleware chain. -func (h *Handler) AliceConstructor() alice.Constructor { - return func(next http.Handler) (http.Handler, error) { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if h == nil { - next.ServeHTTP(rw, req) - return - } - - h.ServeHTTP(rw, req, next) - }), nil - } -} - // NewHandler creates a new Handler. func NewHandler(ctx context.Context, config *otypes.AccessLog) (*Handler, error) { var file io.WriteCloser = noopCloser{os.Stdout} @@ -183,28 +169,18 @@ func NewHandler(ctx context.Context, config *otypes.AccessLog) (*Handler, error) return logHandler, nil } -func openAccessLogFile(filePath string) (*os.File, error) { - dir := filepath.Dir(filePath) +// AliceConstructor returns an alice.Constructor that wraps the Handler (conditionally) in a middleware chain. +func (h *Handler) AliceConstructor() alice.Constructor { + return func(next http.Handler) (http.Handler, error) { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if h == nil { + next.ServeHTTP(rw, req) + return + } - if err := os.MkdirAll(dir, 0o755); err != nil { - return nil, fmt.Errorf("failed to create log path %s: %w", dir, err) + h.ServeHTTP(rw, req, next) + }), nil } - - file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o664) - if err != nil { - return nil, fmt.Errorf("error opening file %s: %w", filePath, err) - } - - return file, nil -} - -// GetLogData gets the request context object that contains logging data. -// This creates data as the request passes through the middleware chain. -func GetLogData(req *http.Request) *LogData { - if ld, ok := req.Context().Value(DataTableKey).(*LogData); ok { - return ld - } - return nil } func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.Handler) { @@ -335,23 +311,6 @@ func (h *Handler) Rotate() error { return nil } -func silentSplitHostPort(value string) (host, port string) { - host, port, err := net.SplitHostPort(value) - if err != nil { - return value, "-" - } - return host, port -} - -func usernameIfPresent(theURL *url.URL) string { - if theURL.User != nil { - if name := theURL.User.Username(); name != "" { - return name - } - } - return "-" -} - // Logging handler to log frontend name, backend name, and elapsed time. func (h *Handler) logTheRoundTrip(ctx context.Context, logDataTable *LogData) { core := logDataTable.Core @@ -458,6 +417,47 @@ func (h *Handler) keepAccessLog(statusCode, retryAttempts int, duration time.Dur return false } +// GetLogData gets the request context object that contains logging data. +// This creates data as the request passes through the middleware chain. +func GetLogData(req *http.Request) *LogData { + if ld, ok := req.Context().Value(DataTableKey).(*LogData); ok { + return ld + } + return nil +} + +func openAccessLogFile(filePath string) (*os.File, error) { + dir := filepath.Dir(filePath) + + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, fmt.Errorf("failed to create log path %s: %w", dir, err) + } + + file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o664) + if err != nil { + return nil, fmt.Errorf("error opening file %s: %w", filePath, err) + } + + return file, nil +} + +func silentSplitHostPort(value string) (host, port string) { + host, port, err := net.SplitHostPort(value) + if err != nil { + return value, "-" + } + return host, port +} + +func usernameIfPresent(theURL *url.URL) string { + if theURL.User != nil { + if name := theURL.User.Username(); name != "" { + return name + } + } + return "-" +} + var requestCounter uint64 // Request ID func nextRequestCount() uint64 { diff --git a/pkg/middlewares/accesslog/logger_formatters.go b/pkg/middlewares/accesslog/logger_formatters.go index 3da18c239..ec1c04f05 100644 --- a/pkg/middlewares/accesslog/logger_formatters.go +++ b/pkg/middlewares/accesslog/logger_formatters.go @@ -81,7 +81,7 @@ func (f *GenericCLFLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { return b.Bytes(), err } -func toLog(fields logrus.Fields, key, defaultValue string, quoted bool) interface{} { +func toLog(fields logrus.Fields, key, defaultValue string, quoted bool) any { if v, ok := fields[key]; ok { if v == nil { return defaultValue diff --git a/pkg/middlewares/accesslog/logger_formatters_test.go b/pkg/middlewares/accesslog/logger_formatters_test.go index a7e85c5d4..c1647a2a9 100644 --- a/pkg/middlewares/accesslog/logger_formatters_test.go +++ b/pkg/middlewares/accesslog/logger_formatters_test.go @@ -15,12 +15,12 @@ func TestCommonLogFormatter_Format(t *testing.T) { testCases := []struct { name string - data map[string]interface{} + data map[string]any expectedLog string }{ { name: "DownstreamStatus & DownstreamContentSize are nil", - data: map[string]interface{}{ + data: map[string]any{ StartUTC: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), Duration: 123 * time.Second, ClientHost: "10.0.0.1", @@ -41,7 +41,7 @@ func TestCommonLogFormatter_Format(t *testing.T) { }, { name: "all data", - data: map[string]interface{}{ + data: map[string]any{ StartUTC: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), Duration: 123 * time.Second, ClientHost: "10.0.0.1", @@ -62,7 +62,7 @@ func TestCommonLogFormatter_Format(t *testing.T) { }, { name: "all data with local time", - data: map[string]interface{}{ + data: map[string]any{ StartLocal: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), Duration: 123 * time.Second, ClientHost: "10.0.0.1", @@ -106,12 +106,12 @@ func TestGenericCLFLogFormatter_Format(t *testing.T) { testCases := []struct { name string - data map[string]interface{} + data map[string]any expectedLog string }{ { name: "DownstreamStatus & DownstreamContentSize are nil", - data: map[string]interface{}{ + data: map[string]any{ StartUTC: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), Duration: 123 * time.Second, ClientHost: "10.0.0.1", @@ -132,7 +132,7 @@ func TestGenericCLFLogFormatter_Format(t *testing.T) { }, { name: "all data", - data: map[string]interface{}{ + data: map[string]any{ StartUTC: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), Duration: 123 * time.Second, ClientHost: "10.0.0.1", @@ -153,7 +153,7 @@ func TestGenericCLFLogFormatter_Format(t *testing.T) { }, { name: "all data with local time", - data: map[string]interface{}{ + data: map[string]any{ StartLocal: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), Duration: 123 * time.Second, ClientHost: "10.0.0.1", @@ -199,7 +199,7 @@ func Test_toLog(t *testing.T) { fieldName string defaultValue string quoted bool - expectedLog interface{} + expectedLog any }{ { desc: "Should return int 1", diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index 8062f59eb..2ccbffb66 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -261,7 +261,7 @@ func lineCount(t *testing.T, fileName string) int { } count := 0 - for _, line := range strings.Split(string(fileContents), "\n") { + for line := range strings.SplitSeq(string(fileContents), "\n") { if strings.TrimSpace(line) == "" { continue } @@ -439,32 +439,32 @@ func TestLoggerGenericCLFWithBufferingSize(t *testing.T) { assertValidGenericCLFLogData(t, expectedLog, logData) } -func assertString(exp string) func(t *testing.T, actual interface{}) { - return func(t *testing.T, actual interface{}) { +func assertString(exp string) func(t *testing.T, actual any) { + return func(t *testing.T, actual any) { t.Helper() assert.Equal(t, exp, actual) } } -func assertNotEmpty() func(t *testing.T, actual interface{}) { - return func(t *testing.T, actual interface{}) { +func assertNotEmpty() func(t *testing.T, actual any) { + return func(t *testing.T, actual any) { t.Helper() assert.NotEmpty(t, actual) } } -func assertFloat64(exp float64) func(t *testing.T, actual interface{}) { - return func(t *testing.T, actual interface{}) { +func assertFloat64(exp float64) func(t *testing.T, actual any) { + return func(t *testing.T, actual any) { t.Helper() assert.InDelta(t, exp, actual, delta) } } -func assertFloat64NotZero() func(t *testing.T, actual interface{}) { - return func(t *testing.T, actual interface{}) { +func assertFloat64NotZero() func(t *testing.T, actual any) { + return func(t *testing.T, actual any) { t.Helper() assert.NotZero(t, actual) @@ -477,7 +477,7 @@ func TestLoggerJSON(t *testing.T) { config *otypes.AccessLog tls bool tracing bool - expected map[string]func(t *testing.T, value interface{}) + expected map[string]func(t *testing.T, value any) }{ { desc: "default config without tracing", @@ -485,7 +485,7 @@ func TestLoggerJSON(t *testing.T) { FilePath: "", Format: JSONFormat, }, - expected: map[string]func(t *testing.T, value interface{}){ + expected: map[string]func(t *testing.T, value any){ RequestContentSize: assertFloat64(0), RequestHost: assertString(testHostname), RequestAddr: assertString(testHostname), @@ -525,7 +525,7 @@ func TestLoggerJSON(t *testing.T) { Format: JSONFormat, }, tracing: true, - expected: map[string]func(t *testing.T, value interface{}){ + expected: map[string]func(t *testing.T, value any){ RequestContentSize: assertFloat64(0), RequestHost: assertString(testHostname), RequestAddr: assertString(testHostname), @@ -567,7 +567,7 @@ func TestLoggerJSON(t *testing.T) { Format: JSONFormat, }, tls: true, - expected: map[string]func(t *testing.T, value interface{}){ + expected: map[string]func(t *testing.T, value any){ RequestContentSize: assertFloat64(0), RequestHost: assertString(testHostname), RequestAddr: assertString(testHostname), @@ -612,7 +612,7 @@ func TestLoggerJSON(t *testing.T) { DefaultMode: "drop", }, }, - expected: map[string]func(t *testing.T, value interface{}){ + expected: map[string]func(t *testing.T, value any){ "level": assertString("info"), "msg": assertString(""), "time": assertNotEmpty(), @@ -633,7 +633,7 @@ func TestLoggerJSON(t *testing.T) { }, }, }, - expected: map[string]func(t *testing.T, value interface{}){ + expected: map[string]func(t *testing.T, value any){ "level": assertString("info"), "msg": assertString(""), "time": assertNotEmpty(), @@ -651,7 +651,7 @@ func TestLoggerJSON(t *testing.T) { }, }, }, - expected: map[string]func(t *testing.T, value interface{}){ + expected: map[string]func(t *testing.T, value any){ "level": assertString("info"), "msg": assertString(""), "time": assertNotEmpty(), @@ -678,7 +678,7 @@ func TestLoggerJSON(t *testing.T) { }, }, }, - expected: map[string]func(t *testing.T, value interface{}){ + expected: map[string]func(t *testing.T, value any){ RequestHost: assertString(testHostname), "level": assertString("info"), "msg": assertString(""), @@ -704,7 +704,7 @@ func TestLoggerJSON(t *testing.T) { }, }, }, - expected: map[string]func(t *testing.T, value interface{}){ + expected: map[string]func(t *testing.T, value any){ RequestHost: assertString(testHostname), "level": assertString("info"), "msg": assertString(""), @@ -730,7 +730,7 @@ func TestLoggerJSON(t *testing.T) { logData, err := os.ReadFile(logFilePath) require.NoError(t, err) - jsonData := make(map[string]interface{}) + jsonData := make(map[string]any) err = json.Unmarshal(logData, &jsonData) require.NoError(t, err) @@ -744,7 +744,7 @@ func TestLoggerJSON(t *testing.T) { } func TestLogger_AbortedRequest(t *testing.T) { - expected := map[string]func(t *testing.T, value interface{}){ + expected := map[string]func(t *testing.T, value any){ RequestContentSize: assertFloat64(0), RequestHost: assertString(testHostname), RequestAddr: assertString(testHostname), @@ -787,7 +787,7 @@ func TestLogger_AbortedRequest(t *testing.T) { logData, err := os.ReadFile(config.FilePath) require.NoError(t, err) - jsonData := make(map[string]interface{}) + jsonData := make(map[string]any) err = json.Unmarshal(logData, &jsonData) require.NoError(t, err) diff --git a/pkg/middlewares/auth/connectionheader.go b/pkg/middlewares/auth/connectionheader.go index 8b78b9430..d5d4e4653 100644 --- a/pkg/middlewares/auth/connectionheader.go +++ b/pkg/middlewares/auth/connectionheader.go @@ -22,7 +22,7 @@ func RemoveConnectionHeaders(req *http.Request) { } for _, f := range req.Header[connectionHeader] { - for _, sf := range strings.Split(f, ",") { + for sf := range strings.SplitSeq(f, ",") { if sf = textproto.TrimString(sf); sf != "" { req.Header.Del(sf) } diff --git a/pkg/middlewares/capture/capture.go b/pkg/middlewares/capture/capture.go index 04e411a20..669e24268 100644 --- a/pkg/middlewares/capture/capture.go +++ b/pkg/middlewares/capture/capture.go @@ -96,20 +96,6 @@ func (c *Capture) Reset(next http.Handler) http.Handler { }) } -func (c *Capture) renew(rw http.ResponseWriter, req *http.Request) (http.ResponseWriter, *http.Request) { - ctx := context.WithValue(req.Context(), capturedData, c) - newReq := req.WithContext(ctx) - - if newReq.Body != nil { - readCounter := &readCounter{source: newReq.Body} - c.rr = readCounter - newReq.Body = readCounter - } - c.crw = &captureResponseWriter{rw: rw} - - return c.crw, newReq -} - func (c *Capture) ResponseSize() int64 { return c.crw.Size() } @@ -127,6 +113,20 @@ func (c *Capture) RequestSize() int64 { return c.rr.size } +func (c *Capture) renew(rw http.ResponseWriter, req *http.Request) (http.ResponseWriter, *http.Request) { + ctx := context.WithValue(req.Context(), capturedData, c) + newReq := req.WithContext(ctx) + + if newReq.Body != nil { + readCounter := &readCounter{source: newReq.Body} + c.rr = readCounter + newReq.Body = readCounter + } + c.crw = &captureResponseWriter{rw: rw} + + return c.crw, newReq +} + type readCounter struct { // source ReadCloser from where the request body is read. source io.ReadCloser diff --git a/pkg/middlewares/compress/acceptencoding.go b/pkg/middlewares/compress/acceptencoding.go index a35362733..5222363b4 100644 --- a/pkg/middlewares/compress/acceptencoding.go +++ b/pkg/middlewares/compress/acceptencoding.go @@ -63,7 +63,7 @@ func parseAcceptableEncodings(acceptEncoding []string, supportedEncodings map[st var encodings []Encoding for _, line := range acceptEncoding { - for _, item := range strings.Split(strings.ReplaceAll(line, " ", ""), ",") { + for item := range strings.SplitSeq(strings.ReplaceAll(line, " ", ""), ",") { parsed := strings.SplitN(item, ";", 2) if len(parsed) == 0 { continue diff --git a/pkg/middlewares/compress/compress.go b/pkg/middlewares/compress/compress.go index 4ab312cc2..1d82e4a53 100644 --- a/pkg/middlewares/compress/compress.go +++ b/pkg/middlewares/compress/compress.go @@ -167,6 +167,10 @@ func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) { c.chooseHandler(c.getCompressionEncoding(acceptEncoding), rw, req) } +func (c *compress) GetTracingInformation() (string, string) { + return c.name, typeName +} + func (c *compress) chooseHandler(typ string, rw http.ResponseWriter, req *http.Request) { switch typ { case zstdName: @@ -180,10 +184,6 @@ func (c *compress) chooseHandler(typ string, rw http.ResponseWriter, req *http.R } } -func (c *compress) GetTracingInformation() (string, string) { - return c.name, typeName -} - func (c *compress) newGzipHandler() (http.Handler, error) { var wrapper func(http.Handler) http.HandlerFunc var err error diff --git a/pkg/middlewares/compress/compression_handler.go b/pkg/middlewares/compress/compression_handler.go index 07583028b..95cce76ba 100644 --- a/pkg/middlewares/compress/compression_handler.go +++ b/pkg/middlewares/compress/compression_handler.go @@ -156,6 +156,7 @@ func (c *CompressionHandler) putCompressionWriter(writer *compressionWriterWrapp type compressionWriterWrapper struct { CompressionWriter + algo string } diff --git a/pkg/middlewares/compress/compression_handler_test.go b/pkg/middlewares/compress/compression_handler_test.go index 89bbe062b..62d9e5787 100644 --- a/pkg/middlewares/compress/compression_handler_test.go +++ b/pkg/middlewares/compress/compression_handler_test.go @@ -874,10 +874,7 @@ func Test_FlushExcludedContentTypes(t *testing.T) { for len(tb) > 0 { // Write 100 bytes per run // Detection should not be affected (we send 100 bytes) - toWrite := 100 - if toWrite > len(tb) { - toWrite = len(tb) - } + toWrite := min(100, len(tb)) _, err := rw.Write(tb[:toWrite]) require.NoError(t, err) @@ -998,10 +995,7 @@ func Test_FlushIncludedContentTypes(t *testing.T) { for len(tb) > 0 { // Write 100 bytes per run // Detection should not be affected (we send 100 bytes) - toWrite := 100 - if toWrite > len(tb) { - toWrite = len(tb) - } + toWrite := min(100, len(tb)) _, err := rw.Write(tb[:toWrite]) require.NoError(t, err) diff --git a/pkg/middlewares/customerrors/custom_errors.go b/pkg/middlewares/customerrors/custom_errors.go index 6c1a91c4b..193ad43ec 100644 --- a/pkg/middlewares/customerrors/custom_errors.go +++ b/pkg/middlewares/customerrors/custom_errors.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "maps" "net" "net/http" "net/url" @@ -198,16 +199,6 @@ func (cc *codeCatcher) Header() http.Header { return cc.headerMap } -func (cc *codeCatcher) getCode() int { - return cc.code -} - -// isFilteredCode returns whether the codeCatcher received a response code among the ones it is watching, -// and for which the response should be deferred to the error handler. -func (cc *codeCatcher) isFilteredCode() bool { - return cc.caughtFilteredCode -} - func (cc *codeCatcher) Write(buf []byte) (int, error) { // If WriteHeader was already called from the caller, this is a NOOP. // Otherwise, cc.code is actually a 200 here. @@ -233,9 +224,7 @@ func (cc *codeCatcher) WriteHeader(code int) { if code >= 100 && code <= 199 { // Multiple informational status codes can be used, // so here the copy is not appending the values to not repeat them. - for k, v := range cc.Header() { - cc.responseWriter.Header()[k] = v - } + maps.Copy(cc.responseWriter.Header(), cc.Header()) cc.responseWriter.WriteHeader(code) return @@ -253,9 +242,8 @@ func (cc *codeCatcher) WriteHeader(code int) { // The copy is not appending the values, // to not repeat them in case any informational status code has been written. - for k, v := range cc.Header() { - cc.responseWriter.Header()[k] = v - } + maps.Copy(cc.responseWriter.Header(), cc.Header()) + cc.responseWriter.WriteHeader(cc.code) cc.headersSent = true } @@ -288,6 +276,16 @@ func (cc *codeCatcher) Flush() { } } +func (cc *codeCatcher) getCode() int { + return cc.code +} + +// isFilteredCode returns whether the codeCatcher received a response code among the ones it is watching, +// and for which the response should be deferred to the error handler. +func (cc *codeCatcher) isFilteredCode() bool { + return cc.caughtFilteredCode +} + // codeModifier forwards a response back to the client, // while enforcing a given response code. type codeModifier struct { @@ -343,17 +341,14 @@ func (r *codeModifier) WriteHeader(code int) { if code >= 100 && code <= 199 { // Multiple informational status codes can be used, // so here the copy is not appending the values to not repeat them. - for k, v := range r.headerMap { - r.responseWriter.Header()[k] = v - } + maps.Copy(r.responseWriter.Header(), r.headerMap) r.responseWriter.WriteHeader(code) return } - for k, v := range r.headerMap { - r.responseWriter.Header()[k] = v - } + maps.Copy(r.responseWriter.Header(), r.headerMap) + r.responseWriter.WriteHeader(r.code) r.headerSent = true } diff --git a/pkg/middlewares/forwardedheaders/forwarded_header.go b/pkg/middlewares/forwardedheaders/forwarded_header.go index d37d12d44..3f994b0a8 100644 --- a/pkg/middlewares/forwardedheaders/forwarded_header.go +++ b/pkg/middlewares/forwardedheaders/forwarded_header.go @@ -81,18 +81,11 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin }, nil } -func (x *XForwarded) isTrustedIP(ip string) bool { - if x.ipChecker == nil { - return false - } - return x.ipChecker.IsAuthorized(ip) == nil -} - // removeIPv6Zone removes the zone if the given IP is an ipv6 address and it has {zone} information in it, // like "[fe80::d806:a55d:eb1b:49cc%vEthernet (vmxnet3 Ethernet Adapter - Virtual Switch)]:64692". func removeIPv6Zone(clientIP string) string { - if idx := strings.Index(clientIP, "%"); idx != -1 { - return clientIP[:idx] + if before, _, found := strings.Cut(clientIP, "%"); found { + return before } return clientIP } @@ -138,6 +131,28 @@ func forwardedPort(req *http.Request) string { return "80" } +// ServeHTTP implements http.Handler. +func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !x.insecure && !x.isTrustedIP(r.RemoteAddr) { + for _, h := range xHeaders { + unsafeHeader(r.Header).Del(h) + } + } + + x.rewrite(r) + + x.removeConnectionHeaders(r) + + x.next.ServeHTTP(w, r) +} + +func (x *XForwarded) isTrustedIP(ip string) bool { + if x.ipChecker == nil { + return false + } + return x.ipChecker.IsAuthorized(ip) == nil +} + func (x *XForwarded) rewrite(outreq *http.Request) { if clientIP, _, err := net.SplitHostPort(outreq.RemoteAddr); err == nil { clientIP = removeIPv6Zone(clientIP) @@ -186,21 +201,6 @@ func (x *XForwarded) rewrite(outreq *http.Request) { } } -// ServeHTTP implements http.Handler. -func (x *XForwarded) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !x.insecure && !x.isTrustedIP(r.RemoteAddr) { - for _, h := range xHeaders { - unsafeHeader(r.Header).Del(h) - } - } - - x.rewrite(r) - - x.removeConnectionHeaders(r) - - x.next.ServeHTTP(w, r) -} - func (x *XForwarded) removeConnectionHeaders(req *http.Request) { var reqUpType string if httpguts.HeaderValuesContainsToken(req.Header[connection], upgrade) { @@ -209,7 +209,7 @@ func (x *XForwarded) removeConnectionHeaders(req *http.Request) { var connectionHopByHopHeaders []string for _, f := range req.Header[connection] { - for _, sf := range strings.Split(f, ",") { + for sf := range strings.SplitSeq(f, ",") { if sf = textproto.TrimString(sf); sf != "" { // Connection header cannot dictate to remove X- headers managed by Traefik, // as per rfc7230 https://datatracker.ietf.org/doc/html/rfc7230#section-6.1, diff --git a/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go index 36ebbd9db..791e15bf2 100644 --- a/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go +++ b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go @@ -1,6 +1,7 @@ package headermodifier import ( + "maps" "net/http" "net/http/httptest" "testing" @@ -105,9 +106,7 @@ func TestRequestHeaderModifier(t *testing.T) { handler := NewRequestHeaderModifier(t.Context(), next, test.config, "foo-request-header-modifier") req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - for h, v := range test.requestHeaders { - req.Header[h] = v - } + maps.Copy(req.Header, test.requestHeaders) resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) diff --git a/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier_test.go b/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier_test.go index 47a0f3c82..26b0ac8fa 100644 --- a/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier_test.go +++ b/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier_test.go @@ -1,6 +1,7 @@ package headermodifier import ( + "maps" "net/http" "net/http/httptest" "testing" @@ -107,9 +108,7 @@ func TestResponseHeaderModifier(t *testing.T) { req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) resp := httptest.NewRecorder() - for k, v := range test.responseHeaders { - resp.Header()[k] = v - } + maps.Copy(resp.Header(), test.responseHeaders) handler.ServeHTTP(resp, req) diff --git a/pkg/middlewares/headers/header.go b/pkg/middlewares/headers/header.go index 6679ce2a8..374938571 100644 --- a/pkg/middlewares/headers/header.go +++ b/pkg/middlewares/headers/header.go @@ -64,27 +64,6 @@ func (s *Header) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } -// modifyCustomRequestHeaders sets or deletes custom request headers. -func (s *Header) modifyCustomRequestHeaders(req *http.Request) { - // Loop through Custom request headers - for header, value := range s.headers.CustomRequestHeaders { - switch { - // Handling https://github.com/golang/go/commit/ecdbffd4ec68b509998792f120868fec319de59b. - case value == "" && header == forward.XForwardedFor: - req.Header[header] = nil - - case value == "": - req.Header.Del(header) - - case strings.EqualFold(header, "Host"): - req.Host = value - - default: - req.Header.Set(header, value) - } - } -} - // PostRequestModifyResponseHeaders set or delete response headers. // This method is called AFTER the response is generated from the backend // and can merge/override headers from the backend response. @@ -134,6 +113,27 @@ func (s *Header) PostRequestModifyResponseHeaders(res *http.Response) error { return nil } +// modifyCustomRequestHeaders sets or deletes custom request headers. +func (s *Header) modifyCustomRequestHeaders(req *http.Request) { + // Loop through Custom request headers + for header, value := range s.headers.CustomRequestHeaders { + switch { + // Handling https://github.com/golang/go/commit/ecdbffd4ec68b509998792f120868fec319de59b. + case value == "" && header == forward.XForwardedFor: + req.Header[header] = nil + + case value == "": + req.Header.Del(header) + + case strings.EqualFold(header, "Host"): + req.Host = value + + default: + req.Header.Set(header, value) + } + } +} + // processCorsHeaders processes the incoming request, // and returns if it is a preflight request. // If not a preflight, it handles the preRequestModifyCorsResponseHeaders. diff --git a/pkg/middlewares/observability/observability.go b/pkg/middlewares/observability/observability.go index 1490a1e8e..a02ca292d 100644 --- a/pkg/middlewares/observability/observability.go +++ b/pkg/middlewares/observability/observability.go @@ -65,7 +65,7 @@ func DetailedTracingEnabled(ctx context.Context) bool { } // SetStatusErrorf flags the span as in error and log an event. -func SetStatusErrorf(ctx context.Context, format string, args ...interface{}) { +func SetStatusErrorf(ctx context.Context, format string, args ...any) { if span := trace.SpanFromContext(ctx); span != nil { span.SetStatus(codes.Error, fmt.Sprintf(format, args...)) } diff --git a/pkg/middlewares/observability/status_code.go b/pkg/middlewares/observability/status_code.go index ebada5579..3686dbad1 100644 --- a/pkg/middlewares/observability/status_code.go +++ b/pkg/middlewares/observability/status_code.go @@ -13,6 +13,7 @@ func newStatusCodeRecorder(rw http.ResponseWriter, status int) *statusCodeRecord type statusCodeRecorder struct { http.ResponseWriter + status int } diff --git a/pkg/middlewares/ratelimiter/lua.go b/pkg/middlewares/ratelimiter/lua.go index 47100629e..fcc84d16a 100644 --- a/pkg/middlewares/ratelimiter/lua.go +++ b/pkg/middlewares/ratelimiter/lua.go @@ -7,14 +7,14 @@ import ( ) type Rediser interface { - Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd - EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd + Eval(ctx context.Context, script string, keys []string, args ...any) *redis.Cmd + EvalSha(ctx context.Context, sha1 string, keys []string, args ...any) *redis.Cmd ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd ScriptLoad(ctx context.Context, script string) *redis.StringCmd Del(ctx context.Context, keys ...string) *redis.IntCmd - EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd - EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd + EvalRO(ctx context.Context, script string, keys []string, args ...any) *redis.Cmd + EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...any) *redis.Cmd } //nolint:dupword diff --git a/pkg/middlewares/ratelimiter/rate_limiter.go b/pkg/middlewares/ratelimiter/rate_limiter.go index fcc553e15..0f107a413 100755 --- a/pkg/middlewares/ratelimiter/rate_limiter.go +++ b/pkg/middlewares/ratelimiter/rate_limiter.go @@ -61,10 +61,7 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name return nil, fmt.Errorf("getting source extractor: %w", err) } - burst := config.Burst - if burst < 1 { - burst = 1 - } + burst := max(config.Burst, 1) period := time.Duration(config.Period) if period < 0 { diff --git a/pkg/middlewares/ratelimiter/rate_limiter_test.go b/pkg/middlewares/ratelimiter/rate_limiter_test.go index a724c6fe6..b1c6e38f1 100644 --- a/pkg/middlewares/ratelimiter/rate_limiter_test.go +++ b/pkg/middlewares/ratelimiter/rate_limiter_test.go @@ -300,11 +300,8 @@ func TestInMemoryRateLimit(t *testing.T) { stop := time.Now() elapsed := stop.Sub(start) - burst := test.config.Burst - if burst < 1 { - // actual default value - burst = 1 - } + // actual default value if burst < 1 + burst := max(test.config.Burst, 1) period := time.Duration(test.config.Period) if period == 0 { @@ -510,11 +507,8 @@ func TestRedisRateLimit(t *testing.T) { stop := time.Now() elapsed := stop.Sub(start) - burst := test.config.Burst - if burst < 1 { - // actual default value - burst = 1 - } + // actual default value + burst := max(test.config.Burst, 1) period := time.Duration(test.config.Period) if period == 0 { @@ -570,7 +564,7 @@ func newMockRedisClient(ttl int) Rediser { } } -func (m *mockRedisClient) EvalSha(ctx context.Context, _ string, keys []string, args ...interface{}) *redis.Cmd { +func (m *mockRedisClient) EvalSha(ctx context.Context, _ string, keys []string, args ...any) *redis.Cmd { state := lua.NewState() defer state.Close() @@ -641,7 +635,7 @@ func (m *mockRedisClient) EvalSha(ctx context.Context, _ string, keys []string, return cmd } - var resultSlice []interface{} + var resultSlice []any resultTable.ForEach(func(_ lua.LValue, value lua.LValue) { valueNbr, ok := value.(lua.LNumber) if !ok { @@ -661,7 +655,7 @@ func (m *mockRedisClient) EvalSha(ctx context.Context, _ string, keys []string, return cmd } -func (m *mockRedisClient) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { +func (m *mockRedisClient) Eval(ctx context.Context, script string, keys []string, args ...any) *redis.Cmd { return m.EvalSha(ctx, script, keys, args...) } @@ -677,11 +671,11 @@ func (m *mockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd return nil } -func (m *mockRedisClient) EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { +func (m *mockRedisClient) EvalRO(ctx context.Context, script string, keys []string, args ...any) *redis.Cmd { return nil } -func (m *mockRedisClient) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { +func (m *mockRedisClient) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...any) *redis.Cmd { return nil } diff --git a/pkg/middlewares/ratelimiter/redis_limiter.go b/pkg/middlewares/ratelimiter/redis_limiter.go index 6bdf6c8fc..9c49c629a 100644 --- a/pkg/middlewares/ratelimiter/redis_limiter.go +++ b/pkg/middlewares/ratelimiter/redis_limiter.go @@ -91,7 +91,7 @@ func (r *redisLimiter) evaluateScript(ctx context.Context, key string) (bool, *t return true, nil, nil } - params := []interface{}{ + params := []any{ float64(r.rate / 1000000), r.burst, r.ttl, @@ -103,7 +103,7 @@ func (r *redisLimiter) evaluateScript(ctx context.Context, key string) (bool, *t return false, nil, fmt.Errorf("running script: %w", err) } - values := v.([]interface{}) + values := v.([]any) ok, err := strconv.ParseBool(values[0].(string)) if err != nil { return false, nil, fmt.Errorf("parsing ok value from redis rate lua script: %w", err) diff --git a/pkg/middlewares/recovery/recovery.go b/pkg/middlewares/recovery/recovery.go index 1d825a7ad..32c4e15f8 100644 --- a/pkg/middlewares/recovery/recovery.go +++ b/pkg/middlewares/recovery/recovery.go @@ -56,7 +56,7 @@ func recoverFunc(rw recoveryResponseWriter, req *http.Request) { // https://github.com/golang/go/blob/a0d6420d8be2ae7164797051ec74fa2a2df466a1/src/net/http/server.go#L1761-L1775 // https://github.com/golang/go/blob/c33153f7b416c03983324b3e8f869ce1116d84bc/src/net/http/httputil/reverseproxy.go#L284 -func shouldLogPanic(panicValue interface{}) bool { +func shouldLogPanic(panicValue any) bool { //nolint:errorlint // false-positive because panicValue is an interface. return panicValue != nil && panicValue != http.ErrAbortHandler } diff --git a/pkg/middlewares/retry/retry.go b/pkg/middlewares/retry/retry.go index de15a21b4..da1fbc543 100644 --- a/pkg/middlewares/retry/retry.go +++ b/pkg/middlewares/retry/retry.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "maps" "math" "net" "net/http" @@ -249,10 +250,7 @@ func (r *responseWriter) WriteHeader(code int) { // to write headers to the backend : we are not going to perform any further retry. // So it is now safe to alter current response headers with headers collected during // the latest try before writing headers to client. - headers := r.responseWriter.Header() - for header, value := range r.headers { - headers[header] = value - } + maps.Copy(r.responseWriter.Header(), r.headers) r.responseWriter.WriteHeader(code) diff --git a/pkg/muxer/http/matcher.go b/pkg/muxer/http/matcher.go index 74f5d97f0..9794fdf94 100644 --- a/pkg/muxer/http/matcher.go +++ b/pkg/muxer/http/matcher.go @@ -184,13 +184,7 @@ func header(tree *matchersTree, headers ...string) error { key, value := http.CanonicalHeaderKey(headers[0]), headers[1] tree.matcher = func(req *http.Request) bool { - for _, headerValue := range req.Header[key] { - if headerValue == value { - return true - } - } - - return false + return slices.Contains(req.Header[key], value) } return nil @@ -205,13 +199,7 @@ func headerRegexp(tree *matchersTree, headers ...string) error { } tree.matcher = func(req *http.Request) bool { - for _, headerValue := range req.Header[key] { - if re.MatchString(headerValue) { - return true - } - } - - return false + return slices.ContainsFunc(req.Header[key], re.MatchString) } return nil diff --git a/pkg/muxer/http/mux.go b/pkg/muxer/http/mux.go index d3ab6a45d..7a936e5a8 100644 --- a/pkg/muxer/http/mux.go +++ b/pkg/muxer/http/mux.go @@ -22,7 +22,8 @@ type MatcherFunc func(*http.Request) bool // Muxer handles routing with rules. type Muxer struct { - routes routes + routes routes + parser SyntaxParser defaultHandler http.Handler } diff --git a/pkg/muxer/tcp/matcher.go b/pkg/muxer/tcp/matcher.go index 4ee55eb68..c3357028e 100644 --- a/pkg/muxer/tcp/matcher.go +++ b/pkg/muxer/tcp/matcher.go @@ -3,6 +3,7 @@ package tcp import ( "fmt" "regexp" + "slices" "strings" "unicode/utf8" @@ -37,13 +38,7 @@ func alpn(tree *matchersTree, protos ...string) error { } tree.matcher = func(meta ConnData) bool { - for _, alpnProto := range meta.alpnProtos { - if alpnProto == proto { - return true - } - } - - return false + return slices.Contains(meta.alpnProtos, proto) } return nil diff --git a/pkg/muxer/tcp/matcher_v2.go b/pkg/muxer/tcp/matcher_v2.go index 4fd0e3368..807159994 100644 --- a/pkg/muxer/tcp/matcher_v2.go +++ b/pkg/muxer/tcp/matcher_v2.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "regexp" + "slices" "strconv" "strings" @@ -56,10 +57,8 @@ func alpnV2(tree *matchersTree, protos ...string) error { tree.matcher = func(meta ConnData) bool { for _, proto := range meta.alpnProtos { - for _, filter := range protos { - if proto == filter { - return true - } + if slices.Contains(protos, proto) { + return true } } diff --git a/pkg/muxer/tcp/mux.go b/pkg/muxer/tcp/mux.go index 1266091ea..db80d8289 100644 --- a/pkg/muxer/tcp/mux.go +++ b/pkg/muxer/tcp/mux.go @@ -121,7 +121,7 @@ func GetRulePriority(rule string) int { // AddRoute adds a new route, associated to the given handler, at the given // priority, to the muxer. func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.Handler) error { - var parse interface{} + var parse any var err error var matcherFuncs map[string]func(*matchersTree, ...string) error diff --git a/pkg/observability/logs/aws.go b/pkg/observability/logs/aws.go index f9040ba4b..13dc0dbf6 100644 --- a/pkg/observability/logs/aws.go +++ b/pkg/observability/logs/aws.go @@ -7,10 +7,10 @@ import ( func NewAWSWrapper(logger zerolog.Logger) logging.LoggerFunc { if logger.GetLevel() > zerolog.DebugLevel { - return func(classification logging.Classification, format string, args ...interface{}) {} + return func(classification logging.Classification, format string, args ...any) {} } - return func(classification logging.Classification, format string, args ...interface{}) { + return func(classification logging.Classification, format string, args ...any) { logger.Debug().CallerSkipFrame(2).MsgFunc(msgFunc(args...)) } } diff --git a/pkg/observability/logs/elastic.go b/pkg/observability/logs/elastic.go index 557ebf194..aef4e7385 100644 --- a/pkg/observability/logs/elastic.go +++ b/pkg/observability/logs/elastic.go @@ -10,10 +10,10 @@ func NewElasticLogger(logger zerolog.Logger) *ElasticLogger { return &ElasticLogger{logger: logger} } -func (l ElasticLogger) Debugf(format string, args ...interface{}) { +func (l ElasticLogger) Debugf(format string, args ...any) { l.logger.Debug().CallerSkipFrame(1).Msgf(format, args...) } -func (l ElasticLogger) Errorf(format string, args ...interface{}) { +func (l ElasticLogger) Errorf(format string, args ...any) { l.logger.Error().CallerSkipFrame(1).Msgf(format, args...) } diff --git a/pkg/observability/logs/gokit.go b/pkg/observability/logs/gokit.go index 1430f8e54..71ea96a95 100644 --- a/pkg/observability/logs/gokit.go +++ b/pkg/observability/logs/gokit.go @@ -7,10 +7,10 @@ import ( func NewGoKitWrapper(logger zerolog.Logger) kitlog.LoggerFunc { if logger.GetLevel() > zerolog.DebugLevel { - return func(args ...interface{}) error { return nil } + return func(args ...any) error { return nil } } - return func(args ...interface{}) error { + return func(args ...any) error { logger.Debug().CallerSkipFrame(2).MsgFunc(msgFunc(args...)) return nil } diff --git a/pkg/observability/logs/hclog.go b/pkg/observability/logs/hclog.go index 19d120d17..61de63c5e 100644 --- a/pkg/observability/logs/hclog.go +++ b/pkg/observability/logs/hclog.go @@ -21,26 +21,26 @@ func NewRetryableHTTPLogger(logger zerolog.Logger) *RetryableHTTPLogger { } // Error starts a new message with error level. -func (l RetryableHTTPLogger) Error(msg string, keysAndValues ...interface{}) { +func (l RetryableHTTPLogger) Error(msg string, keysAndValues ...any) { logWithLevel(l.logger.Error().CallerSkipFrame(2), msg, keysAndValues...) } // Info starts a new message with info level. -func (l RetryableHTTPLogger) Info(msg string, keysAndValues ...interface{}) { +func (l RetryableHTTPLogger) Info(msg string, keysAndValues ...any) { logWithLevel(l.logger.Info().CallerSkipFrame(2), msg, keysAndValues...) } // Debug starts a new message with debug level. -func (l RetryableHTTPLogger) Debug(msg string, keysAndValues ...interface{}) { +func (l RetryableHTTPLogger) Debug(msg string, keysAndValues ...any) { logWithLevel(l.logger.Debug().CallerSkipFrame(2), msg, keysAndValues...) } // Warn starts a new message with warn level. -func (l RetryableHTTPLogger) Warn(msg string, keysAndValues ...interface{}) { +func (l RetryableHTTPLogger) Warn(msg string, keysAndValues ...any) { logWithLevel(l.logger.Warn().CallerSkipFrame(2), msg, keysAndValues...) } -func logWithLevel(ev *zerolog.Event, msg string, kvs ...interface{}) { +func logWithLevel(ev *zerolog.Event, msg string, kvs ...any) { if len(kvs)%2 == 0 { for i := 0; i < len(kvs)-1; i += 2 { // The first item of the pair (the key) is supposed to be a string. diff --git a/pkg/observability/logs/instana.go b/pkg/observability/logs/instana.go index be4f522d6..cefee69dd 100644 --- a/pkg/observability/logs/instana.go +++ b/pkg/observability/logs/instana.go @@ -12,18 +12,18 @@ func NewInstanaLogger(logger zerolog.Logger) *InstanaLogger { return &InstanaLogger{logger: logger} } -func (l InstanaLogger) Debug(args ...interface{}) { +func (l InstanaLogger) Debug(args ...any) { l.logger.Debug().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } -func (l InstanaLogger) Info(args ...interface{}) { +func (l InstanaLogger) Info(args ...any) { l.logger.Info().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } -func (l InstanaLogger) Warn(args ...interface{}) { +func (l InstanaLogger) Warn(args ...any) { l.logger.Warn().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } -func (l InstanaLogger) Error(args ...interface{}) { +func (l InstanaLogger) Error(args ...any) { l.logger.Error().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } diff --git a/pkg/observability/logs/logrus.go b/pkg/observability/logs/logrus.go index 68de8555b..8e1b0ec33 100644 --- a/pkg/observability/logs/logrus.go +++ b/pkg/observability/logs/logrus.go @@ -12,38 +12,38 @@ func NewLogrusWrapper(logger zerolog.Logger) *LogrusStdWrapper { return &LogrusStdWrapper{logger: logger} } -func (l LogrusStdWrapper) Print(args ...interface{}) { +func (l LogrusStdWrapper) Print(args ...any) { l.logger.Debug().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } -func (l LogrusStdWrapper) Printf(s string, args ...interface{}) { +func (l LogrusStdWrapper) Printf(s string, args ...any) { l.logger.Debug().CallerSkipFrame(1).Msgf(s, args...) } -func (l LogrusStdWrapper) Println(args ...interface{}) { +func (l LogrusStdWrapper) Println(args ...any) { l.logger.Debug().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } -func (l LogrusStdWrapper) Fatal(args ...interface{}) { +func (l LogrusStdWrapper) Fatal(args ...any) { l.logger.Fatal().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } -func (l LogrusStdWrapper) Fatalf(s string, args ...interface{}) { +func (l LogrusStdWrapper) Fatalf(s string, args ...any) { l.logger.Fatal().CallerSkipFrame(1).Msgf(s, args...) } -func (l LogrusStdWrapper) Fatalln(args ...interface{}) { +func (l LogrusStdWrapper) Fatalln(args ...any) { l.logger.Fatal().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } -func (l LogrusStdWrapper) Panic(args ...interface{}) { +func (l LogrusStdWrapper) Panic(args ...any) { l.logger.Panic().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } -func (l LogrusStdWrapper) Panicf(s string, args ...interface{}) { +func (l LogrusStdWrapper) Panicf(s string, args ...any) { l.logger.Panic().CallerSkipFrame(1).Msgf(s, args...) } -func (l LogrusStdWrapper) Panicln(args ...interface{}) { +func (l LogrusStdWrapper) Panicln(args ...any) { l.logger.Panic().CallerSkipFrame(1).MsgFunc(msgFunc(args...)) } diff --git a/pkg/observability/logs/oxy.go b/pkg/observability/logs/oxy.go index 6f0f90aed..db615eff9 100644 --- a/pkg/observability/logs/oxy.go +++ b/pkg/observability/logs/oxy.go @@ -10,18 +10,18 @@ func NewOxyWrapper(logger zerolog.Logger) *OxyWrapper { return &OxyWrapper{logger: logger} } -func (l OxyWrapper) Debug(s string, i ...interface{}) { +func (l OxyWrapper) Debug(s string, i ...any) { l.logger.Debug().CallerSkipFrame(1).Msgf(s, i...) } -func (l OxyWrapper) Info(s string, i ...interface{}) { +func (l OxyWrapper) Info(s string, i ...any) { l.logger.Info().CallerSkipFrame(1).Msgf(s, i...) } -func (l OxyWrapper) Warn(s string, i ...interface{}) { +func (l OxyWrapper) Warn(s string, i ...any) { l.logger.Warn().CallerSkipFrame(1).Msgf(s, i...) } -func (l OxyWrapper) Error(s string, i ...interface{}) { +func (l OxyWrapper) Error(s string, i ...any) { l.logger.Error().CallerSkipFrame(1).Msgf(s, i...) } diff --git a/pkg/observability/metrics/headers.go b/pkg/observability/metrics/headers.go index 65443cd29..ed165ae96 100644 --- a/pkg/observability/metrics/headers.go +++ b/pkg/observability/metrics/headers.go @@ -36,16 +36,16 @@ func (c MultiCounterWithHeaders) With(headers http.Header, labelValues ...string return next } -// NewCounterWithNoopHeaders returns a CounterWithNoopHeaders. -func NewCounterWithNoopHeaders(counter metrics.Counter) CounterWithNoopHeaders { - return CounterWithNoopHeaders{counter: counter} -} - // CounterWithNoopHeaders is a counter that satisfies CounterWithHeaders but ignores the given http.Header. type CounterWithNoopHeaders struct { counter metrics.Counter } +// NewCounterWithNoopHeaders returns a CounterWithNoopHeaders. +func NewCounterWithNoopHeaders(counter metrics.Counter) CounterWithNoopHeaders { + return CounterWithNoopHeaders{counter: counter} +} + // Add adds the given delta value to the counter value. func (c CounterWithNoopHeaders) Add(delta float64) { c.counter.Add(delta) diff --git a/pkg/observability/metrics/otel.go b/pkg/observability/metrics/otel.go index eaf95e736..39fbde94e 100644 --- a/pkg/observability/metrics/otel.go +++ b/pkg/observability/metrics/otel.go @@ -506,7 +506,7 @@ func (lvs otelLabelNamesValues) With(labelValues ...string) otelLabelNamesValues // to the native attribute.KeyValue. func (lvs otelLabelNamesValues) ToLabels() []attribute.KeyValue { labels := make([]attribute.KeyValue, len(lvs)/2) - for i := 0; i < len(labels); i++ { + for i := range labels { labels[i] = attribute.String(lvs[2*i], lvs[2*i+1]) } return labels diff --git a/pkg/observability/tracing/tracing.go b/pkg/observability/tracing/tracing.go index bb38bc654..012d3dcd2 100644 --- a/pkg/observability/tracing/tracing.go +++ b/pkg/observability/tracing/tracing.go @@ -29,6 +29,15 @@ type Backend interface { Setup(ctx context.Context, serviceName string, sampleRate float64, resourceAttributes map[string]string) (trace.Tracer, io.Closer, error) } +// Tracer is trace.Tracer with additional properties. +type Tracer struct { + trace.Tracer + + safeQueryParams []string + capturedRequestHeaders []string + capturedResponseHeaders []string +} + // NewTracing Creates a Tracing. func NewTracing(ctx context.Context, conf *static.Tracing) (*Tracer, io.Closer, error) { var backend Backend @@ -57,76 +66,6 @@ func NewTracing(ctx context.Context, conf *static.Tracing) (*Tracer, io.Closer, return NewTracer(tr, conf.CapturedRequestHeaders, conf.CapturedResponseHeaders, conf.SafeQueryParams), closer, nil } -// TracerFromContext extracts the trace.Tracer from the given context. -func TracerFromContext(ctx context.Context) *Tracer { - // Prevent picking trace.noopSpan tracer. - if !trace.SpanContextFromContext(ctx).IsValid() { - return nil - } - - span := trace.SpanFromContext(ctx) - if span != nil && span.TracerProvider() != nil { - tracer := span.TracerProvider().Tracer("github.com/traefik/traefik") - if tracer, ok := tracer.(*Tracer); ok { - return tracer - } - - return nil - } - - return nil -} - -// ExtractCarrierIntoContext reads cross-cutting concerns from the carrier into a Context. -func ExtractCarrierIntoContext(ctx context.Context, headers http.Header) context.Context { - propagator := otel.GetTextMapPropagator() - return propagator.Extract(ctx, propagation.HeaderCarrier(headers)) -} - -// InjectContextIntoCarrier sets cross-cutting concerns from the request context into the request headers. -func InjectContextIntoCarrier(req *http.Request) { - propagator := otel.GetTextMapPropagator() - propagator.Inject(req.Context(), propagation.HeaderCarrier(req.Header)) -} - -// Span is trace.Span wrapping the Traefik TracerProvider. -type Span struct { - trace.Span - - tracerProvider *TracerProvider -} - -// TracerProvider returns the span's TraceProvider. -func (s Span) TracerProvider() trace.TracerProvider { - return s.tracerProvider -} - -// TracerProvider is trace.TracerProvider wrapping the Traefik Tracer implementation. -type TracerProvider struct { - trace.TracerProvider - - tracer *Tracer -} - -// Tracer returns the trace.Tracer for the given options. -// It returns specifically the Traefik Tracer when requested. -func (t TracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer { - if name == "github.com/traefik/traefik" { - return t.tracer - } - - return t.TracerProvider.Tracer(name, options...) -} - -// Tracer is trace.Tracer with additional properties. -type Tracer struct { - trace.Tracer - - safeQueryParams []string - capturedRequestHeaders []string - capturedResponseHeaders []string -} - // NewTracer builds and configures a new Tracer. func NewTracer(tracer trace.Tracer, capturedRequestHeaders, capturedResponseHeaders, safeQueryParams []string) *Tracer { return &Tracer{ @@ -299,6 +238,67 @@ func (t *Tracer) safeURL(originalURL *url.URL) *url.URL { return &redactedURL } +// Span is trace.Span wrapping the Traefik TracerProvider. +type Span struct { + trace.Span + + tracerProvider *TracerProvider +} + +// TracerProvider returns the span's TraceProvider. +func (s Span) TracerProvider() trace.TracerProvider { + return s.tracerProvider +} + +// TracerProvider is trace.TracerProvider wrapping the Traefik Tracer implementation. +type TracerProvider struct { + trace.TracerProvider + + tracer *Tracer +} + +// Tracer returns the trace.Tracer for the given options. +// It returns specifically the Traefik Tracer when requested. +func (t TracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer { + if name == "github.com/traefik/traefik" { + return t.tracer + } + + return t.TracerProvider.Tracer(name, options...) +} + +// TracerFromContext extracts the trace.Tracer from the given context. +func TracerFromContext(ctx context.Context) *Tracer { + // Prevent picking trace.noopSpan tracer. + if !trace.SpanContextFromContext(ctx).IsValid() { + return nil + } + + span := trace.SpanFromContext(ctx) + if span != nil && span.TracerProvider() != nil { + tracer := span.TracerProvider().Tracer("github.com/traefik/traefik") + if tracer, ok := tracer.(*Tracer); ok { + return tracer + } + + return nil + } + + return nil +} + +// ExtractCarrierIntoContext reads cross-cutting concerns from the carrier into a Context. +func ExtractCarrierIntoContext(ctx context.Context, headers http.Header) context.Context { + propagator := otel.GetTextMapPropagator() + return propagator.Extract(ctx, propagation.HeaderCarrier(headers)) +} + +// InjectContextIntoCarrier sets cross-cutting concerns from the request context into the request headers. +func InjectContextIntoCarrier(req *http.Request) { + propagator := otel.GetTextMapPropagator() + propagator.Inject(req.Context(), propagation.HeaderCarrier(req.Header)) +} + func proto(proto string) string { switch proto { case "HTTP/1.0": diff --git a/pkg/observability/tracing/tracing_test.go b/pkg/observability/tracing/tracing_test.go index 91dc19e60..698a1f435 100644 --- a/pkg/observability/tracing/tracing_test.go +++ b/pkg/observability/tracing/tracing_test.go @@ -492,6 +492,8 @@ func resourceAttributes(traces ptrace.Traces) map[string]string { } // mainSpan gets the main span from traces (assumes single span for testing). +// +//nolint:unqueryvet // False positive: This is OTel trace iteration, not SQLBoiler. func mainSpan(traces ptrace.Traces) ptrace.Span { for _, resourceSpans := range traces.ResourceSpans().All() { for _, scopeSpans := range resourceSpans.ScopeSpans().All() { diff --git a/pkg/plugins/builder.go b/pkg/plugins/builder.go index 96a4bf21e..cee4c9497 100644 --- a/pkg/plugins/builder.go +++ b/pkg/plugins/builder.go @@ -18,7 +18,7 @@ type pluginMiddleware interface { } type middlewareBuilder interface { - newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) + newMiddleware(config map[string]any, middlewareName string) (pluginMiddleware, error) } // Builder is a plugin builder. @@ -110,7 +110,7 @@ func NewBuilder(manager *Manager, plugins map[string]Descriptor, localPlugins ma } // Build builds a middleware plugin. -func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (Constructor, error) { +func (b Builder) Build(pName string, config map[string]any, middlewareName string) (Constructor, error) { if b.middlewareBuilders == nil { return nil, fmt.Errorf("no plugin definitions in the static configuration: %s", pName) } diff --git a/pkg/plugins/manager_test.go b/pkg/plugins/manager_test.go index 5100b2a6b..be5717c19 100644 --- a/pkg/plugins/manager_test.go +++ b/pkg/plugins/manager_test.go @@ -51,7 +51,7 @@ func TestPluginManager_ReadManifest(t *testing.T) { Type: "middleware", Import: "github.com/test/plugin", Summary: "A test plugin", - TestData: map[string]interface{}{ + TestData: map[string]any{ "test": "data", }, } diff --git a/pkg/plugins/middlewarewasm.go b/pkg/plugins/middlewarewasm.go index 48d179426..e72b53510 100644 --- a/pkg/plugins/middlewarewasm.go +++ b/pkg/plugins/middlewarewasm.go @@ -42,7 +42,7 @@ func newWasmMiddlewareBuilder(goPath, moduleName, wasmPath string, settings Sett return &wasmMiddlewareBuilder{path: path, cache: cache, settings: settings}, nil } -func (b wasmMiddlewareBuilder) newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) { +func (b wasmMiddlewareBuilder) newMiddleware(config map[string]any, middlewareName string) (pluginMiddleware, error) { return &WasmMiddleware{ middlewareName: middlewareName, config: reflect.ValueOf(config), @@ -114,7 +114,7 @@ func (b *wasmMiddlewareBuilder) buildMiddleware(ctx context.Context, next http.H i := cfg.Interface() if i != nil { - config, ok := i.(map[string]interface{}) + config, ok := i.(map[string]any) if !ok { return nil, nil, fmt.Errorf("could not type assert config: %T", i) } diff --git a/pkg/plugins/middlewarewasm_test.go b/pkg/plugins/middlewarewasm_test.go index faa33942f..5f270952b 100644 --- a/pkg/plugins/middlewarewasm_test.go +++ b/pkg/plugins/middlewarewasm_test.go @@ -27,12 +27,12 @@ func TestSettingsWithoutSocket(t *testing.T) { testCases := []struct { desc string - getSettings func(t *testing.T) (Settings, map[string]interface{}) + getSettings func(t *testing.T) (Settings, map[string]any) expected string }{ { desc: "mounts path", - getSettings: func(t *testing.T) (Settings, map[string]interface{}) { + getSettings: func(t *testing.T) (Settings, map[string]any) { t.Helper() tempDir := t.TempDir() @@ -42,7 +42,7 @@ func TestSettingsWithoutSocket(t *testing.T) { return Settings{Mounts: []string{ tempDir, - }}, map[string]interface{}{ + }}, map[string]any{ "file": filePath, } }, @@ -50,7 +50,7 @@ func TestSettingsWithoutSocket(t *testing.T) { }, { desc: "mounts src to dest", - getSettings: func(t *testing.T) (Settings, map[string]interface{}) { + getSettings: func(t *testing.T) (Settings, map[string]any) { t.Helper() tempDir := t.TempDir() @@ -60,7 +60,7 @@ func TestSettingsWithoutSocket(t *testing.T) { return Settings{Mounts: []string{ tempDir + ":/tmp", - }}, map[string]interface{}{ + }}, map[string]any{ "file": "/tmp/hello.txt", } }, @@ -68,11 +68,11 @@ func TestSettingsWithoutSocket(t *testing.T) { }, { desc: "one env", - getSettings: func(t *testing.T) (Settings, map[string]interface{}) { + getSettings: func(t *testing.T) (Settings, map[string]any) { t.Helper() envs := []string{"PLUGIN_TEST"} - return Settings{Envs: envs}, map[string]interface{}{ + return Settings{Envs: envs}, map[string]any{ "envs": envs, } }, @@ -80,11 +80,11 @@ func TestSettingsWithoutSocket(t *testing.T) { }, { desc: "two env", - getSettings: func(t *testing.T) (Settings, map[string]interface{}) { + getSettings: func(t *testing.T) (Settings, map[string]any) { t.Helper() envs := []string{"PLUGIN_TEST", "PLUGIN_TEST_B"} - return Settings{Envs: envs}, map[string]interface{}{ + return Settings{Envs: envs}, map[string]any{ "envs": envs, } }, diff --git a/pkg/plugins/middlewareyaegi.go b/pkg/plugins/middlewareyaegi.go index 41d120158..77f642b4b 100644 --- a/pkg/plugins/middlewareyaegi.go +++ b/pkg/plugins/middlewareyaegi.go @@ -46,7 +46,7 @@ func newYaegiMiddlewareBuilder(i *interp.Interpreter, basePkg, imp string) (*yae }, nil } -func (b yaegiMiddlewareBuilder) newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) { +func (b yaegiMiddlewareBuilder) newMiddleware(config map[string]any, middlewareName string) (pluginMiddleware, error) { vConfig, err := b.createConfig(config) if err != nil { return nil, err @@ -80,7 +80,7 @@ func (b yaegiMiddlewareBuilder) newHandler(ctx context.Context, next http.Handle return handler, nil } -func (b yaegiMiddlewareBuilder) createConfig(config map[string]interface{}) (reflect.Value, error) { +func (b yaegiMiddlewareBuilder) createConfig(config map[string]any) (reflect.Value, error) { results := b.fnCreateConfig.Call(nil) if len(results) != 1 { return reflect.Value{}, fmt.Errorf("invalid number of return for the CreateConfig function: %d", len(results)) diff --git a/pkg/plugins/middlewareyaegi_test.go b/pkg/plugins/middlewareyaegi_test.go index f9df8232d..0094da8c5 100644 --- a/pkg/plugins/middlewareyaegi_test.go +++ b/pkg/plugins/middlewareyaegi_test.go @@ -138,7 +138,7 @@ func verifyMiddlewareWorks(t *testing.T, builder *yaegiMiddlewareBuilder) { t.Helper() // Create a middleware instance - this will call the plugin's New() function // which uses unsafe/syscall, proving they work - middleware, err := builder.newMiddleware(map[string]interface{}{ + middleware, err := builder.newMiddleware(map[string]any{ "message": "test", }, "test-middleware") require.NoError(t, err, "Should be able to create middleware that uses unsafe/syscall") diff --git a/pkg/plugins/providers.go b/pkg/plugins/providers.go index 46c6df8cc..76df8c82e 100644 --- a/pkg/plugins/providers.go +++ b/pkg/plugins/providers.go @@ -25,7 +25,7 @@ type PP interface { } type _PP struct { - IValue interface{} + IValue any WInit func() error WProvide func(cfgChan chan<- json.Marshaler) error WStop func() error @@ -53,7 +53,7 @@ func ppSymbols() map[string]map[string]reflect.Value { } // BuildProvider builds a plugin's provider. -func (b Builder) BuildProvider(pName string, config map[string]interface{}) (provider.Provider, error) { +func (b Builder) BuildProvider(pName string, config map[string]any) (provider.Provider, error) { if b.providerBuilders == nil { return nil, fmt.Errorf("no plugin definition in the static configuration: %s", pName) } @@ -82,7 +82,7 @@ type Provider struct { pp PP } -func newProvider(builder providerBuilder, config map[string]interface{}, providerName string) (*Provider, error) { +func newProvider(builder providerBuilder, config map[string]any, providerName string) (*Provider, error) { basePkg := builder.BasePkg if basePkg == "" { basePkg = strings.ReplaceAll(path.Base(builder.Import), "-", "_") diff --git a/pkg/plugins/types.go b/pkg/plugins/types.go index aa05cc320..5be52eec7 100644 --- a/pkg/plugins/types.go +++ b/pkg/plugins/types.go @@ -42,16 +42,16 @@ type LocalDescriptor struct { // Manifest The plugin manifest. type Manifest struct { - DisplayName string `yaml:"displayName"` - Type string `yaml:"type"` - Runtime string `yaml:"runtime"` - WasmPath string `yaml:"wasmPath"` - Import string `yaml:"import"` - BasePkg string `yaml:"basePkg"` - Compatibility string `yaml:"compatibility"` - Summary string `yaml:"summary"` - UseUnsafe bool `yaml:"useUnsafe"` - TestData map[string]interface{} `yaml:"testData"` + DisplayName string `yaml:"displayName"` + Type string `yaml:"type"` + Runtime string `yaml:"runtime"` + WasmPath string `yaml:"wasmPath"` + Import string `yaml:"import"` + BasePkg string `yaml:"basePkg"` + Compatibility string `yaml:"compatibility"` + Summary string `yaml:"summary"` + UseUnsafe bool `yaml:"useUnsafe"` + TestData map[string]any `yaml:"testData"` } // IsYaegiPlugin returns true if the plugin is a Yaegi plugin. diff --git a/pkg/provider/acme/local_store.go b/pkg/provider/acme/local_store.go index dab258822..87b9f5475 100644 --- a/pkg/provider/acme/local_store.go +++ b/pkg/provider/acme/local_store.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "io" + "maps" "os" "sync" @@ -30,6 +31,52 @@ func NewLocalStore(filename string, routinesPool *safe.Pool) *LocalStore { return store } +// GetAccount returns ACME Account. +func (s *LocalStore) GetAccount(resolverName string) (*Account, error) { + storedData, err := s.get(resolverName) + if err != nil { + return nil, err + } + + return storedData.Account, nil +} + +// SaveAccount stores ACME Account. +func (s *LocalStore) SaveAccount(resolverName string, account *Account) error { + storedData, err := s.get(resolverName) + if err != nil { + return err + } + + storedData.Account = account + s.save(resolverName, storedData) + + return nil +} + +// GetCertificates returns ACME Certificates list. +func (s *LocalStore) GetCertificates(resolverName string) ([]*CertAndStore, error) { + storedData, err := s.get(resolverName) + if err != nil { + return nil, err + } + + return storedData.Certificates, nil +} + +// SaveCertificates stores ACME Certificates list. +func (s *LocalStore) SaveCertificates(resolverName string, certificates []*CertAndStore) error { + storedData, err := s.get(resolverName) + if err != nil { + return err + } + + storedData.Certificates = certificates + s.save(resolverName, storedData) + + return nil +} + func (s *LocalStore) save(resolverName string, storedData *StoredData) { s.lock.Lock() defer s.lock.Unlock() @@ -122,8 +169,7 @@ func (s *LocalStore) listenSaveAction(routinesPool *safe.Pool) { logger.Error().Err(err).Send() } - err = os.WriteFile(s.filename, data, 0o600) - if err != nil { + if err := os.WriteFile(s.filename, data, 0o600); err != nil { logger.Error().Err(err).Send() } } @@ -133,55 +179,5 @@ func (s *LocalStore) listenSaveAction(routinesPool *safe.Pool) { // unSafeCopyOfStoredData creates maps copy of storedData. Is not thread safe, you should use `s.lock`. func (s *LocalStore) unSafeCopyOfStoredData() map[string]*StoredData { - result := map[string]*StoredData{} - for k, v := range s.storedData { - result[k] = v - } - return result -} - -// GetAccount returns ACME Account. -func (s *LocalStore) GetAccount(resolverName string) (*Account, error) { - storedData, err := s.get(resolverName) - if err != nil { - return nil, err - } - - return storedData.Account, nil -} - -// SaveAccount stores ACME Account. -func (s *LocalStore) SaveAccount(resolverName string, account *Account) error { - storedData, err := s.get(resolverName) - if err != nil { - return err - } - - storedData.Account = account - s.save(resolverName, storedData) - - return nil -} - -// GetCertificates returns ACME Certificates list. -func (s *LocalStore) GetCertificates(resolverName string) ([]*CertAndStore, error) { - storedData, err := s.get(resolverName) - if err != nil { - return nil, err - } - - return storedData.Certificates, nil -} - -// SaveCertificates stores ACME Certificates list. -func (s *LocalStore) SaveCertificates(resolverName string, certificates []*CertAndStore) error { - storedData, err := s.get(resolverName) - if err != nil { - return err - } - - storedData.Certificates = certificates - s.save(resolverName, storedData) - - return nil + return maps.Clone(s.storedData) } diff --git a/pkg/provider/acme/local_store_unix.go b/pkg/provider/acme/local_store_unix.go index 163ecd7ae..60f85ce27 100644 --- a/pkg/provider/acme/local_store_unix.go +++ b/pkg/provider/acme/local_store_unix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package acme diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go index f8ab4ca1f..fb181dd54 100644 --- a/pkg/provider/acme/provider.go +++ b/pkg/provider/acme/provider.go @@ -78,6 +78,7 @@ func (a *Configuration) SetDefaults() { // CertAndStore allows mapping a TLS certificate to a TLS store. type CertAndStore struct { Certificate + Store string } @@ -127,6 +128,7 @@ type TLSChallenge struct { // Provider holds configurations of the provider. type Provider struct { *Configuration + ResolverName string Store Store `json:"store,omitempty" toml:"store,omitempty" yaml:"store,omitempty"` @@ -1085,7 +1087,7 @@ func (p *Provider) certExists(validDomains []string) bool { func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool { for _, certDomains := range existentDomains { - for _, certDomain := range strings.Split(certDomains, ",") { + for certDomain := range strings.SplitSeq(certDomains, ",") { if types.MatchDomain(domainToCheck, certDomain) { return true } diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index 95bb0b220..b68ef0551 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -150,13 +150,6 @@ func NewProviderAggregator(conf static.Providers) *ProviderAggregator { return p } -func (p *ProviderAggregator) quietAddProvider(provider provider.Provider) { - err := p.AddProvider(provider) - if err != nil { - log.Error().Err(err).Msgf("Error while initializing provider %T", provider) - } -} - // AddProvider adds a provider in the providers map. func (p *ProviderAggregator) AddProvider(provider provider.Provider) error { err := provider.Init() @@ -202,6 +195,13 @@ func (p *ProviderAggregator) Provide(configurationChan chan<- dynamic.Message, p return nil } +func (p *ProviderAggregator) quietAddProvider(provider provider.Provider) { + err := p.AddProvider(provider) + if err != nil { + log.Error().Err(err).Msgf("Error while initializing provider %T", provider) + } +} + func (p *ProviderAggregator) launchProvider(configurationChan chan<- dynamic.Message, pool *safe.Pool, prd provider.Provider) { jsonConf, err := redactor.RemoveCredentials(prd) if err != nil { diff --git a/pkg/provider/configuration.go b/pkg/provider/configuration.go index 2872ed7e3..d310881f8 100644 --- a/pkg/provider/configuration.go +++ b/pkg/provider/configuration.go @@ -401,9 +401,7 @@ func MakeDefaultRuleTemplate(defaultRule string, funcMap template.FuncMap) (*tem defaultFuncMap := sprig.TxtFuncMap() defaultFuncMap["normalize"] = Normalize - for k, fn := range funcMap { - defaultFuncMap[k] = fn - } + maps.Copy(defaultFuncMap, funcMap) return template.New("defaultRule").Funcs(defaultFuncMap).Parse(defaultRule) } @@ -458,7 +456,7 @@ func BuildUDPRouterConfiguration(ctx context.Context, configuration *dynamic.UDP } // BuildRouterConfiguration builds a router configuration. -func BuildRouterConfiguration(ctx context.Context, configuration *dynamic.HTTPConfiguration, defaultRouterName string, defaultRuleTpl *template.Template, model interface{}) { +func BuildRouterConfiguration(ctx context.Context, configuration *dynamic.HTTPConfiguration, defaultRouterName string, defaultRuleTpl *template.Template, model any) { if len(configuration.Routers) == 0 { if len(configuration.Services) > 1 { log.Ctx(ctx).Info().Msg("Could not create a router for the container: too many services") diff --git a/pkg/provider/constraints/constraints_labels.go b/pkg/provider/constraints/constraints_labels.go index 6d74bfbab..a8dd20aca 100644 --- a/pkg/provider/constraints/constraints_labels.go +++ b/pkg/provider/constraints/constraints_labels.go @@ -24,7 +24,7 @@ func MatchLabels(labels map[string]string, expr string) (bool, error) { NOT: notLabelFunc, OR: orLabelFunc, }, - Functions: map[string]interface{}{ + Functions: map[string]any{ "Label": labelFn, "LabelRegex": labelRegexFn, }, diff --git a/pkg/provider/constraints/constraints_tags.go b/pkg/provider/constraints/constraints_tags.go index b61f43c5d..ad8fa8fc9 100644 --- a/pkg/provider/constraints/constraints_tags.go +++ b/pkg/provider/constraints/constraints_tags.go @@ -25,7 +25,7 @@ func MatchTags(tags []string, expr string) (bool, error) { NOT: notTagFunc, OR: orTagFunc, }, - Functions: map[string]interface{}{ + Functions: map[string]any{ "Tag": tagFn, "TagRegex": tagRegexFn, }, diff --git a/pkg/provider/consulcatalog/consul_catalog.go b/pkg/provider/consulcatalog/consul_catalog.go index ec3fb64da..9da53ce26 100644 --- a/pkg/provider/consulcatalog/consul_catalog.go +++ b/pkg/provider/consulcatalog/consul_catalog.go @@ -248,6 +248,11 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } +// Namespace returns the namespace of the ConsulCatalog provider. +func (p *Provider) Namespace() string { + return p.namespace +} + func (p *Provider) loadConfiguration(ctx context.Context, certInfo *connectCert, configurationChan chan<- dynamic.Message) error { data, err := p.getConsulServicesData(ctx) if err != nil { @@ -388,12 +393,12 @@ func (p *Provider) fetchService(ctx context.Context, name string, connectEnabled // watchServices watches for update events of the services list and statuses, // and transmits them to the caller through the p.watchServicesChan. func (p *Provider) watchServices(ctx context.Context) error { - servicesWatcher, err := watch.Parse(map[string]interface{}{"type": "services"}) + servicesWatcher, err := watch.Parse(map[string]any{"type": "services"}) if err != nil { return fmt.Errorf("failed to create services watcher plan: %w", err) } - servicesWatcher.HybridHandler = func(_ watch.BlockingParamVal, _ interface{}) { + servicesWatcher.HybridHandler = func(_ watch.BlockingParamVal, _ any) { select { case <-ctx.Done(): case p.watchServicesChan <- struct{}{}: @@ -402,12 +407,12 @@ func (p *Provider) watchServices(ctx context.Context) error { } } - checksWatcher, err := watch.Parse(map[string]interface{}{"type": "checks"}) + checksWatcher, err := watch.Parse(map[string]any{"type": "checks"}) if err != nil { return fmt.Errorf("failed to create checks watcher plan: %w", err) } - checksWatcher.HybridHandler = func(_ watch.BlockingParamVal, _ interface{}) { + checksWatcher.HybridHandler = func(_ watch.BlockingParamVal, _ any) { select { case <-ctx.Done(): case p.watchServicesChan <- struct{}{}: @@ -447,66 +452,11 @@ func (p *Provider) watchServices(ctx context.Context) error { } } -func rootsWatchHandler(ctx context.Context, dest chan<- []string) func(watch.BlockingParamVal, interface{}) { - return func(_ watch.BlockingParamVal, raw interface{}) { - if raw == nil { - log.Ctx(ctx).Error().Msg("Root certificate watcher called with nil") - return - } - - v, ok := raw.(*api.CARootList) - if !ok || v == nil { - log.Ctx(ctx).Error().Msg("Invalid result for root certificate watcher") - return - } - - roots := make([]string, 0, len(v.Roots)) - for _, root := range v.Roots { - roots = append(roots, root.RootCertPEM) - } - - select { - case <-ctx.Done(): - case dest <- roots: - } - } -} - -type keyPair struct { - cert string - key string -} - -func leafWatcherHandler(ctx context.Context, dest chan<- keyPair) func(watch.BlockingParamVal, interface{}) { - return func(_ watch.BlockingParamVal, raw interface{}) { - if raw == nil { - log.Ctx(ctx).Error().Msg("Leaf certificate watcher called with nil") - return - } - - v, ok := raw.(*api.LeafCert) - if !ok || v == nil { - log.Ctx(ctx).Error().Msg("Invalid result for leaf certificate watcher") - return - } - - kp := keyPair{ - cert: v.CertPEM, - key: v.PrivateKeyPEM, - } - - select { - case <-ctx.Done(): - case dest <- kp: - } - } -} - // watchConnectTLS watches for updates of the root certificate or the leaf // certificate, and transmits them to the caller via p.certChan. func (p *Provider) watchConnectTLS(ctx context.Context) error { leafChan := make(chan keyPair) - leafWatcher, err := watch.Parse(map[string]interface{}{ + leafWatcher, err := watch.Parse(map[string]any{ "type": "connect_leaf", "service": p.ServiceName, }) @@ -516,7 +466,7 @@ func (p *Provider) watchConnectTLS(ctx context.Context) error { leafWatcher.HybridHandler = leafWatcherHandler(ctx, leafChan) rootsChan := make(chan []string) - rootsWatcher, err := watch.Parse(map[string]interface{}{ + rootsWatcher, err := watch.Parse(map[string]any{ "type": "connect_roots", }) if err != nil { @@ -596,6 +546,61 @@ func (p *Provider) includesHealthStatus(status string) bool { return false } +func rootsWatchHandler(ctx context.Context, dest chan<- []string) func(watch.BlockingParamVal, any) { + return func(_ watch.BlockingParamVal, raw any) { + if raw == nil { + log.Ctx(ctx).Error().Msg("Root certificate watcher called with nil") + return + } + + v, ok := raw.(*api.CARootList) + if !ok || v == nil { + log.Ctx(ctx).Error().Msg("Invalid result for root certificate watcher") + return + } + + roots := make([]string, 0, len(v.Roots)) + for _, root := range v.Roots { + roots = append(roots, root.RootCertPEM) + } + + select { + case <-ctx.Done(): + case dest <- roots: + } + } +} + +type keyPair struct { + cert string + key string +} + +func leafWatcherHandler(ctx context.Context, dest chan<- keyPair) func(watch.BlockingParamVal, any) { + return func(_ watch.BlockingParamVal, raw any) { + if raw == nil { + log.Ctx(ctx).Error().Msg("Leaf certificate watcher called with nil") + return + } + + v, ok := raw.(*api.LeafCert) + if !ok || v == nil { + log.Ctx(ctx).Error().Msg("Invalid result for leaf certificate watcher") + return + } + + kp := keyPair{ + cert: v.CertPEM, + key: v.PrivateKeyPEM, + } + + select { + case <-ctx.Done(): + case dest <- kp: + } + } +} + func createClient(namespace string, endpoint *EndpointConfig) (*api.Client, error) { config := api.Config{ Address: endpoint.Address, @@ -647,8 +652,3 @@ func repeatSend(ctx context.Context, interval time.Duration, c chan<- struct{}) } } } - -// Namespace returns the namespace of the ConsulCatalog provider. -func (p *Provider) Namespace() string { - return p.namespace -} diff --git a/pkg/provider/docker/config.go b/pkg/provider/docker/config.go index c0140655e..96133db4f 100644 --- a/pkg/provider/docker/config.go +++ b/pkg/provider/docker/config.go @@ -20,6 +20,7 @@ import ( type DynConfBuilder struct { Shared + apiClient client.APIClient swarm bool } diff --git a/pkg/provider/docker/pdocker.go b/pkg/provider/docker/pdocker.go index 684e97c11..937f6cdea 100644 --- a/pkg/provider/docker/pdocker.go +++ b/pkg/provider/docker/pdocker.go @@ -50,10 +50,6 @@ func (p *Provider) Init() error { return nil } -func (p *Provider) createClient(ctx context.Context) (*client.Client, error) { - return createClient(ctx, p.ClientConfig) -} - // Provide allows the docker provider to provide configurations to traefik using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { pool.GoCtx(func(routineCtx context.Context) { @@ -160,6 +156,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } +func (p *Provider) createClient(ctx context.Context) (*client.Client, error) { + return createClient(ctx, p.ClientConfig) +} + func (p *Provider) listContainers(ctx context.Context, dockerClient client.ContainerAPIClient) ([]dockerData, error) { containerList, err := dockerClient.ContainerList(ctx, container.ListOptions{ All: true, diff --git a/pkg/provider/docker/pswarm.go b/pkg/provider/docker/pswarm.go index f596b9e67..d16df07f8 100644 --- a/pkg/provider/docker/pswarm.go +++ b/pkg/provider/docker/pswarm.go @@ -54,10 +54,6 @@ func (p *SwarmProvider) Init() error { return nil } -func (p *SwarmProvider) createClient(ctx context.Context) (*client.Client, error) { - return createClient(ctx, p.ClientConfig) -} - // Provide allows the docker provider to provide configurations to traefik using the given configuration channel. func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { pool.GoCtx(func(routineCtx context.Context) { @@ -154,6 +150,10 @@ func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool * return nil } +func (p *SwarmProvider) createClient(ctx context.Context) (*client.Client, error) { + return createClient(ctx, p.ClientConfig) +} + func (p *SwarmProvider) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) { logger := log.Ctx(ctx) diff --git a/pkg/provider/docker/pswarm_mock_test.go b/pkg/provider/docker/pswarm_mock_test.go index 8793982b9..d7af8f312 100644 --- a/pkg/provider/docker/pswarm_mock_test.go +++ b/pkg/provider/docker/pswarm_mock_test.go @@ -12,6 +12,7 @@ import ( type fakeTasksClient struct { dockerclient.APIClient + tasks []swarmtypes.Task container containertypes.InspectResponse err error @@ -27,6 +28,7 @@ func (c *fakeTasksClient) ContainerInspect(ctx context.Context, container string type fakeServicesClient struct { dockerclient.APIClient + dockerVersion string networks []networktypes.Summary nodes []swarmtypes.Node diff --git a/pkg/provider/ecs/ecs.go b/pkg/provider/ecs/ecs.go index 1204387d7..c8186fe7d 100644 --- a/pkg/provider/ecs/ecs.go +++ b/pkg/provider/ecs/ecs.go @@ -104,41 +104,6 @@ func (p *Provider) Init() error { return nil } -func (p *Provider) createClient(ctx context.Context, logger zerolog.Logger) (*awsClient, error) { - optFns := []func(*config.LoadOptions) error{ - config.WithLogger(logs.NewAWSWrapper(logger)), - } - if p.Region != "" { - optFns = append(optFns, config.WithRegion(p.Region)) - } else { - logger.Info().Msg("No region provided, will retrieve region from the EC2 Metadata service") - optFns = append(optFns, config.WithEC2IMDSRegion()) - } - - if p.AccessKeyID != "" && p.SecretAccessKey != "" { - // From https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/configure-gosdk.html#specify-credentials-programmatically: - // "If you explicitly provide credentials, as in this example, the SDK uses only those credentials." - // this makes sure that user-defined credentials always have the highest priority - staticCreds := aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(p.AccessKeyID, p.SecretAccessKey, "")) - optFns = append(optFns, config.WithCredentialsProvider(staticCreds)) - - // If the access key and secret access key are not provided, config.LoadDefaultConfig - // will look for the credentials in the default credential chain. - // See https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/configure-gosdk.html#specifying-credentials. - } - - cfg, err := config.LoadDefaultConfig(ctx, optFns...) - if err != nil { - return nil, err - } - - return &awsClient{ - ecs.NewFromConfig(cfg), - ec2.NewFromConfig(cfg), - ssm.NewFromConfig(cfg), - }, nil -} - // Provide configuration to traefik from ECS. func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { pool.GoCtx(func(routineCtx context.Context) { @@ -185,6 +150,41 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } +func (p *Provider) createClient(ctx context.Context, logger zerolog.Logger) (*awsClient, error) { + optFns := []func(*config.LoadOptions) error{ + config.WithLogger(logs.NewAWSWrapper(logger)), + } + if p.Region != "" { + optFns = append(optFns, config.WithRegion(p.Region)) + } else { + logger.Info().Msg("No region provided, will retrieve region from the EC2 Metadata service") + optFns = append(optFns, config.WithEC2IMDSRegion()) + } + + if p.AccessKeyID != "" && p.SecretAccessKey != "" { + // From https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/configure-gosdk.html#specify-credentials-programmatically: + // "If you explicitly provide credentials, as in this example, the SDK uses only those credentials." + // this makes sure that user-defined credentials always have the highest priority + staticCreds := aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(p.AccessKeyID, p.SecretAccessKey, "")) + optFns = append(optFns, config.WithCredentialsProvider(staticCreds)) + + // If the access key and secret access key are not provided, config.LoadDefaultConfig + // will look for the credentials in the default credential chain. + // See https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/configure-gosdk.html#specifying-credentials. + } + + cfg, err := config.LoadDefaultConfig(ctx, optFns...) + if err != nil { + return nil, err + } + + return &awsClient{ + ecs.NewFromConfig(cfg), + ec2.NewFromConfig(cfg), + ssm.NewFromConfig(cfg), + }, nil +} + func (p *Provider) loadConfiguration(ctx context.Context, client *awsClient, configurationChan chan<- dynamic.Message) error { instances, err := p.listInstances(ctx, client) if err != nil { diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go index 5f0b08223..04395878e 100644 --- a/pkg/provider/file/file.go +++ b/pkg/provider/file/file.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "maps" "os" "os/signal" "path" @@ -112,6 +113,51 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } +// CreateConfiguration creates a provider configuration from content using templating. +func (p *Provider) CreateConfiguration(ctx context.Context, filename string, funcMap template.FuncMap, templateObjects any) (*dynamic.Configuration, error) { + tmplContent, err := readFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading configuration file: %s - %w", filename, err) + } + + defaultFuncMap := sprig.TxtFuncMap() + defaultFuncMap["normalize"] = provider.Normalize + defaultFuncMap["split"] = strings.Split + maps.Copy(defaultFuncMap, funcMap) + + tmpl := template.New(p.Filename).Funcs(defaultFuncMap) + + _, err = tmpl.Parse(tmplContent) + if err != nil { + return nil, err + } + + var buffer bytes.Buffer + err = tmpl.Execute(&buffer, templateObjects) + if err != nil { + return nil, err + } + + renderedTemplate := buffer.String() + if p.DebugLogGeneratedTemplate { + logger := log.Ctx(ctx) + logger.Debug().Msgf("Template content: %s", tmplContent) + logger.Debug().Msgf("Rendering results: %s", renderedTemplate) + } + + return p.decodeConfiguration(filename, renderedTemplate) +} + +// DecodeConfiguration Decodes a *types.Configuration from a content. +func (p *Provider) DecodeConfiguration(filename string) (*dynamic.Configuration, error) { + content, err := readFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading configuration file: %s - %w", filename, err) + } + + return p.decodeConfiguration(filename, content) +} + func (p *Provider) addWatcher(pool *safe.Pool, items []string, configurationChan chan<- dynamic.Message, callback func(chan<- dynamic.Message) error) error { watcher, err := fsnotify.NewWatcher() if err != nil { @@ -185,13 +231,6 @@ func (p *Provider) buildConfiguration() (*dynamic.Configuration, error) { return nil, errors.New("error using file configuration provider, neither filename nor directory is defined") } -func sendConfigToChannel(configurationChan chan<- dynamic.Message, configuration *dynamic.Configuration) { - configurationChan <- dynamic.Message{ - ProviderName: "file", - Configuration: configuration, - } -} - func (p *Provider) loadFileConfig(ctx context.Context, filename string, parseTemplate bool) (*dynamic.Configuration, error) { var err error var configuration *dynamic.Configuration @@ -337,29 +376,6 @@ func (p *Provider) loadFileConfig(ctx context.Context, filename string, parseTem return configuration, nil } -func flattenCertificates(ctx context.Context, tlsConfig *dynamic.TLSConfiguration) []*tls.CertAndStores { - var certs []*tls.CertAndStores - for _, cert := range tlsConfig.Certificates { - content, err := cert.Certificate.CertFile.Read() - if err != nil { - log.Ctx(ctx).Error().Err(err).Send() - continue - } - cert.Certificate.CertFile = types.FileOrContent(string(content)) - - content, err = cert.Certificate.KeyFile.Read() - if err != nil { - log.Ctx(ctx).Error().Err(err).Send() - continue - } - cert.Certificate.KeyFile = types.FileOrContent(string(content)) - - certs = append(certs, cert) - } - - return certs -} - func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory string, configuration *dynamic.Configuration) (*dynamic.Configuration, error) { fileList, err := os.ReadDir(directory) if err != nil { @@ -539,53 +555,6 @@ func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory st return configuration, nil } -// CreateConfiguration creates a provider configuration from content using templating. -func (p *Provider) CreateConfiguration(ctx context.Context, filename string, funcMap template.FuncMap, templateObjects interface{}) (*dynamic.Configuration, error) { - tmplContent, err := readFile(filename) - if err != nil { - return nil, fmt.Errorf("error reading configuration file: %s - %w", filename, err) - } - - defaultFuncMap := sprig.TxtFuncMap() - defaultFuncMap["normalize"] = provider.Normalize - defaultFuncMap["split"] = strings.Split - for funcID, funcElement := range funcMap { - defaultFuncMap[funcID] = funcElement - } - - tmpl := template.New(p.Filename).Funcs(defaultFuncMap) - - _, err = tmpl.Parse(tmplContent) - if err != nil { - return nil, err - } - - var buffer bytes.Buffer - err = tmpl.Execute(&buffer, templateObjects) - if err != nil { - return nil, err - } - - renderedTemplate := buffer.String() - if p.DebugLogGeneratedTemplate { - logger := log.Ctx(ctx) - logger.Debug().Msgf("Template content: %s", tmplContent) - logger.Debug().Msgf("Rendering results: %s", renderedTemplate) - } - - return p.decodeConfiguration(filename, renderedTemplate) -} - -// DecodeConfiguration Decodes a *types.Configuration from a content. -func (p *Provider) DecodeConfiguration(filename string) (*dynamic.Configuration, error) { - content, err := readFile(filename) - if err != nil { - return nil, fmt.Errorf("error reading configuration file: %s - %w", filename, err) - } - - return p.decodeConfiguration(filename, content) -} - func (p *Provider) decodeConfiguration(filePath, content string) (*dynamic.Configuration, error) { configuration := &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ @@ -618,6 +587,36 @@ func (p *Provider) decodeConfiguration(filePath, content string) (*dynamic.Confi return configuration, nil } +func sendConfigToChannel(configurationChan chan<- dynamic.Message, configuration *dynamic.Configuration) { + configurationChan <- dynamic.Message{ + ProviderName: "file", + Configuration: configuration, + } +} + +func flattenCertificates(ctx context.Context, tlsConfig *dynamic.TLSConfiguration) []*tls.CertAndStores { + var certs []*tls.CertAndStores + for _, cert := range tlsConfig.Certificates { + content, err := cert.Certificate.CertFile.Read() + if err != nil { + log.Ctx(ctx).Error().Err(err).Send() + continue + } + cert.Certificate.CertFile = types.FileOrContent(string(content)) + + content, err = cert.Certificate.KeyFile.Read() + if err != nil { + log.Ctx(ctx).Error().Err(err).Send() + continue + } + cert.Certificate.KeyFile = types.FileOrContent(string(content)) + + certs = append(certs, cert) + } + + return certs +} + func readFile(filename string) (string, error) { if len(filename) > 0 { buf, err := os.ReadFile(filename) diff --git a/pkg/provider/kubernetes/crd/client.go b/pkg/provider/kubernetes/crd/client.go index dfaa7584a..19896b727 100644 --- a/pkg/provider/kubernetes/crd/client.go +++ b/pkg/provider/kubernetes/crd/client.go @@ -34,7 +34,7 @@ const resyncPeriod = 10 * time.Minute // WatchAll starts the watch of the Provider resources and updates the stores. // The stores can then be accessed via the Get* functions. type Client interface { - WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) + WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan any, error) GetIngressRoutes() []*traefikv1alpha1.IngressRoute GetIngressRouteTCPs() []*traefikv1alpha1.IngressRouteTCP GetIngressRouteUDPs() []*traefikv1alpha1.IngressRouteUDP @@ -157,8 +157,8 @@ func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrCon } // WatchAll starts namespace-specific controllers for all relevant kinds. -func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) { - eventCh := make(chan interface{}, 1) +func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan any, error) { + eventCh := make(chan any, 1) eventHandler := &k8s.ResourceEventHandler{Ev: eventCh} if len(namespaces) == 0 { diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 92b45a124..d2fc71306 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -68,55 +68,6 @@ type Provider struct { routerTransform k8s.RouterTransform } -func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) { - p.routerTransform = routerTransform -} - -func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, ingress *traefikv1alpha1.IngressRoute) { - if p.routerTransform == nil { - return - } - - err := p.routerTransform.Apply(ctx, rt, ingress) - if err != nil { - log.Ctx(ctx).Error().Err(err).Msg("Apply router transform") - } -} - -func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { - _, err := labels.Parse(p.LabelSelector) - if err != nil { - return nil, fmt.Errorf("invalid label selector: %q", p.LabelSelector) - } - log.Ctx(ctx).Info().Msgf("label selector is: %q", p.LabelSelector) - - withEndpoint := "" - if p.Endpoint != "" { - withEndpoint = fmt.Sprintf(" with endpoint %s", p.Endpoint) - } - - var client *clientWrapper - switch { - case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "": - log.Ctx(ctx).Info().Msgf("Creating in-cluster Provider client%s", withEndpoint) - client, err = newInClusterClient(p.Endpoint) - case os.Getenv("KUBECONFIG") != "": - log.Ctx(ctx).Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG")) - client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG")) - default: - log.Ctx(ctx).Info().Msgf("Creating cluster-external Provider client%s", withEndpoint) - client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token) - } - - if err != nil { - return nil, err - } - - client.labelSelector = p.LabelSelector - client.disableClusterScopeInformer = p.DisableClusterScopeResources - return client, nil -} - // Init the provider. func (p *Provider) Init() error { return nil @@ -205,6 +156,73 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } +func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) { + p.routerTransform = routerTransform +} + +func (p *Provider) FillExtensionBuilderRegistry(registry gateway.ExtensionBuilderRegistry) { + registry.RegisterFilterFuncs(traefikv1alpha1.GroupName, "Middleware", func(name, namespace string) (string, *dynamic.Middleware, error) { + if len(p.Namespaces) > 0 && !slices.Contains(p.Namespaces, namespace) { + return "", nil, fmt.Errorf("namespace %q is not allowed", namespace) + } + + return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil + }) + + registry.RegisterBackendFuncs(traefikv1alpha1.GroupName, "TraefikService", func(name, namespace string) (string, *dynamic.Service, error) { + if len(p.Namespaces) > 0 && !slices.Contains(p.Namespaces, namespace) { + return "", nil, fmt.Errorf("namespace %q is not allowed", namespace) + } + + return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil + }) +} + +func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, ingress *traefikv1alpha1.IngressRoute) { + if p.routerTransform == nil { + return + } + + err := p.routerTransform.Apply(ctx, rt, ingress) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("Apply router transform") + } +} + +func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { + _, err := labels.Parse(p.LabelSelector) + if err != nil { + return nil, fmt.Errorf("invalid label selector: %q", p.LabelSelector) + } + log.Ctx(ctx).Info().Msgf("label selector is: %q", p.LabelSelector) + + withEndpoint := "" + if p.Endpoint != "" { + withEndpoint = fmt.Sprintf(" with endpoint %s", p.Endpoint) + } + + var client *clientWrapper + switch { + case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "": + log.Ctx(ctx).Info().Msgf("Creating in-cluster Provider client%s", withEndpoint) + client, err = newInClusterClient(p.Endpoint) + case os.Getenv("KUBECONFIG") != "": + log.Ctx(ctx).Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG")) + client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG")) + default: + log.Ctx(ctx).Info().Msgf("Creating cluster-external Provider client%s", withEndpoint) + client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token) + } + + if err != nil { + return nil, err + } + + client.labelSelector = p.LabelSelector + client.disableClusterScopeInformer = p.DisableClusterScopeResources + return client, nil +} + func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) *dynamic.Configuration { stores, tlsConfigs := buildTLSStores(ctx, client) if tlsConfigs == nil { @@ -580,6 +598,32 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) return conf } +func (p *Provider) createErrorPageMiddleware(client Client, namespace string, errorPage *traefikv1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { + if errorPage == nil { + return nil, nil, nil + } + + errorPageMiddleware := &dynamic.ErrorPage{ + Status: errorPage.Status, + StatusRewrites: errorPage.StatusRewrites, + Query: errorPage.Query, + } + + cb := configBuilder{ + client: client, + allowCrossNamespace: p.AllowCrossNamespace, + allowExternalNameServices: p.AllowExternalNameServices, + allowEmptyServices: p.AllowEmptyServices, + } + + balancerServerHTTP, err := cb.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec) + if err != nil { + return nil, nil, err + } + + return errorPageMiddleware, balancerServerHTTP, nil +} + // getServicePort always returns a valid port, an error otherwise. func getServicePort(svc *corev1.Service, port intstr.IntOrString) (*corev1.ServicePort, error) { if svc == nil { @@ -651,7 +695,7 @@ func createPluginMiddleware(k8sClient Client, ns string, plugins map[string]apie return pcMap, nil } -func loadSecretKeys(k8sClient Client, ns string, i interface{}) (interface{}, error) { +func loadSecretKeys(k8sClient Client, ns string, i any) (any, error) { var err error switch iv := i.(type) { case string: @@ -661,14 +705,14 @@ func loadSecretKeys(k8sClient Client, ns string, i interface{}) (interface{}, er return getSecretValue(k8sClient, ns, iv) - case []interface{}: + case []any: for i := range iv { if iv[i], err = loadSecretKeys(k8sClient, ns, iv[i]); err != nil { return nil, err } } - case map[string]interface{}: + case map[string]any: for k := range iv { if iv[k], err = loadSecretKeys(k8sClient, ns, iv[k]); err != nil { return nil, err @@ -901,50 +945,6 @@ func createRetryMiddleware(retry *traefikv1alpha1.Retry) (*dynamic.Retry, error) return r, nil } -func (p *Provider) createErrorPageMiddleware(client Client, namespace string, errorPage *traefikv1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { - if errorPage == nil { - return nil, nil, nil - } - - errorPageMiddleware := &dynamic.ErrorPage{ - Status: errorPage.Status, - StatusRewrites: errorPage.StatusRewrites, - Query: errorPage.Query, - } - - cb := configBuilder{ - client: client, - allowCrossNamespace: p.AllowCrossNamespace, - allowExternalNameServices: p.AllowExternalNameServices, - allowEmptyServices: p.AllowEmptyServices, - } - - balancerServerHTTP, err := cb.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec) - if err != nil { - return nil, nil, err - } - - return errorPageMiddleware, balancerServerHTTP, nil -} - -func (p *Provider) FillExtensionBuilderRegistry(registry gateway.ExtensionBuilderRegistry) { - registry.RegisterFilterFuncs(traefikv1alpha1.GroupName, "Middleware", func(name, namespace string) (string, *dynamic.Middleware, error) { - if len(p.Namespaces) > 0 && !slices.Contains(p.Namespaces, namespace) { - return "", nil, fmt.Errorf("namespace %q is not allowed", namespace) - } - - return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil - }) - - registry.RegisterBackendFuncs(traefikv1alpha1.GroupName, "TraefikService", func(name, namespace string) (string, *dynamic.Service, error) { - if len(p.Namespaces) > 0 && !slices.Contains(p.Namespaces, namespace) { - return "", nil, fmt.Errorf("namespace %q is not allowed", namespace) - } - - return makeID(namespace, name) + providerNamespaceSeparator + providerName, nil, nil - }) -} - func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traefikv1alpha1.ForwardAuth) (*dynamic.ForwardAuth, error) { if auth == nil { return nil, nil @@ -1507,12 +1507,12 @@ func getCABlocksFromConfigMap(configMap *corev1.ConfigMap, namespace, name strin return "", fmt.Errorf("config map %s/%s contains neither tls.ca nor ca.crt", namespace, name) } -func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} { +func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan any) chan any { if throttleDuration == 0 { return nil } // Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling) - eventsChanBuffered := make(chan interface{}, 1) + eventsChanBuffered := make(chan any, 1) // Run a goroutine that reads events from eventChan and does a non-blocking write to pendingEvent. // This guarantees that writing to eventChan will never block, diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 018a3dd43..7845f1d4a 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -4197,7 +4197,7 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{ "default-test-secret": { Plugin: map[string]dynamic.PluginConf{ - "test-secret": map[string]interface{}{ + "test-secret": map[string]any{ "user": "admin", "secret": "this_is_the_secret", }, @@ -4229,10 +4229,10 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{ "default-test-secret": { Plugin: map[string]dynamic.PluginConf{ - "test-secret": map[string]interface{}{ - "secret_0": map[string]interface{}{ - "secret_1": map[string]interface{}{ - "secret_2": map[string]interface{}{ + "test-secret": map[string]any{ + "secret_0": map[string]any{ + "secret_1": map[string]any{ + "secret_2": map[string]any{ "user": "admin", "secret": "this_is_the_very_deep_secret", }, @@ -4267,8 +4267,8 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{ "default-test-secret": { Plugin: map[string]dynamic.PluginConf{ - "test-secret": map[string]interface{}{ - "secret": []interface{}{"secret_data1", "secret_data2"}, + "test-secret": map[string]any{ + "secret": []any{"secret_data1", "secret_data2"}, }, }, }, @@ -4298,13 +4298,13 @@ func TestLoadIngressRoutes(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{ "default-test-secret": { Plugin: map[string]dynamic.PluginConf{ - "test-secret": map[string]interface{}{ - "users": []interface{}{ - map[string]interface{}{ + "test-secret": map[string]any{ + "users": []any{ + map[string]any{ "name": "admin", "secret": "admin_password", }, - map[string]interface{}{ + map[string]any{ "name": "user", "secret": "user_password", }, diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go index 36c50286b..adb068b4b 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go @@ -41,6 +41,7 @@ type Route struct { Priority int `json:"priority,omitempty"` // Syntax defines the router's rule syntax. // More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/http/routing/rules-and-priority/#rulesyntax + // // Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. Syntax string `json:"syntax,omitempty"` // Services defines the list of Service. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go index 1d719ee9a..9a3333825 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go @@ -33,6 +33,7 @@ type RouteTCP struct { // Syntax defines the router's rule syntax. // More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/routing/rules-and-priority/#rulesyntax // +kubebuilder:validation:Enum=v3;v2 + // // Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. Syntax string `json:"syntax,omitempty"` // Services defines the list of TCP services. @@ -82,10 +83,12 @@ type ServiceTCP struct { // hence fully terminating the connection. // It is a duration in milliseconds, defaulting to 100. // A negative value means an infinite deadline (i.e. the reading capability is never closed). + // // Deprecated: TerminationDelay will not be supported in future APIVersions, please use ServersTransport to configure the TerminationDelay instead. TerminationDelay *int `json:"terminationDelay,omitempty"` // ProxyProtocol defines the PROXY protocol configuration. // More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/service/#proxy-protocol + // // Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead. ProxyProtocol *dynamic.ProxyProtocol `json:"proxyProtocol,omitempty"` // ServersTransport defines the name of ServersTransportTCP resource to use. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middlewaretcp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middlewaretcp.go index cf0e2a648..c515a6126 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middlewaretcp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middlewaretcp.go @@ -27,8 +27,9 @@ type MiddlewareTCPSpec struct { InFlightConn *dynamic.TCPInFlightConn `json:"inFlightConn,omitempty"` // IPWhiteList defines the IPWhiteList middleware configuration. // This middleware accepts/refuses connections based on the client IP. - // Deprecated: please use IPAllowList instead. // More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/tcp/middlewares/ipwhitelist/ + // + // Deprecated: please use IPAllowList instead. IPWhiteList *dynamic.TCPIPWhiteList `json:"ipWhiteList,omitempty"` // IPAllowList defines the IPAllowList middleware configuration. // This middleware accepts/refuses connections based on the client IP. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go index 8cfbb92be..48fce5881 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go @@ -34,6 +34,7 @@ type ServersTransportSpec struct { // RootCAs defines a list of CA certificate Secrets or ConfigMaps used to validate server certificates. RootCAs []RootCA `json:"rootCAs,omitempty"` // RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + // // Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"` // CertificatesSecrets defines a list of secret storing client certificates for mTLS. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransporttcp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransporttcp.go index 374108d10..0345e65e2 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransporttcp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransporttcp.go @@ -54,6 +54,7 @@ type TLSClientConfig struct { // RootCAs defines a list of CA certificate Secrets or ConfigMaps used to validate server certificates. RootCAs []RootCA `json:"rootCAs,omitempty"` // RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + // // Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"` // CertificatesSecrets defines a list of secret storing client certificates for mTLS. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go index b4eebf6b7..c556e8a0a 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go @@ -48,6 +48,7 @@ type TLSOptionSpec struct { DisableSessionTickets bool `json:"disableSessionTickets,omitempty"` // PreferServerCipherSuites defines whether the server chooses a cipher suite among his own instead of among the client's. // It is enabled automatically when minVersion or maxVersion is set. + // // Deprecated: https://github.com/golang/go/issues/45430 PreferServerCipherSuites *bool `json:"preferServerCipherSuites,omitempty"` } diff --git a/pkg/provider/kubernetes/gateway/client.go b/pkg/provider/kubernetes/gateway/client.go index 50cf789d3..fd383d89e 100644 --- a/pkg/provider/kubernetes/gateway/client.go +++ b/pkg/provider/kubernetes/gateway/client.go @@ -126,8 +126,8 @@ func newExternalClusterClient(endpoint, caFilePath string, token types.FileOrCon } // WatchAll starts namespace-specific controllers for all relevant kinds. -func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) { - eventCh := make(chan interface{}, 1) +func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan any, error) { + eventCh := make(chan any, 1) eventHandler := &k8s.ResourceEventHandler{Ev: eventCh} if len(namespaces) == 0 { diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index 863c2f093..158014af8 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "maps" "net" "net/http" "regexp" @@ -911,28 +912,20 @@ func mergeHTTPConfiguration(from, to *dynamic.Configuration) { if to.HTTP.Routers == nil { to.HTTP.Routers = map[string]*dynamic.Router{} } - for routerName, router := range from.HTTP.Routers { - to.HTTP.Routers[routerName] = router - } + maps.Copy(to.HTTP.Routers, from.HTTP.Routers) if to.HTTP.Middlewares == nil { to.HTTP.Middlewares = map[string]*dynamic.Middleware{} } - for middlewareName, middleware := range from.HTTP.Middlewares { - to.HTTP.Middlewares[middlewareName] = middleware - } + maps.Copy(to.HTTP.Middlewares, from.HTTP.Middlewares) if to.HTTP.Services == nil { to.HTTP.Services = map[string]*dynamic.Service{} } - for serviceName, service := range from.HTTP.Services { - to.HTTP.Services[serviceName] = service - } + maps.Copy(to.HTTP.Services, from.HTTP.Services) if to.HTTP.ServersTransports == nil { to.HTTP.ServersTransports = map[string]*dynamic.ServersTransport{} } - for name, serversTransport := range from.HTTP.ServersTransports { - to.HTTP.ServersTransports[name] = serversTransport - } + maps.Copy(to.HTTP.ServersTransports, from.HTTP.ServersTransports) } diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 0c50b76a8..59ef2e66b 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -165,49 +165,6 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) { p.routerTransform = routerTransform } -func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, route *gatev1.HTTPRoute) { - if p.routerTransform == nil { - return - } - - if err := p.routerTransform.Apply(ctx, rt, route); err != nil { - log.Ctx(ctx).Error().Err(err).Msg("Apply router transform") - } -} - -func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { - // Label selector validation - _, err := labels.Parse(p.LabelSelector) - if err != nil { - return nil, fmt.Errorf("invalid label selector: %q", p.LabelSelector) - } - - logger := log.Ctx(ctx) - logger.Info().Msgf("Label selector is: %q", p.LabelSelector) - - var client *clientWrapper - switch { - case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "": - logger.Info().Str("endpoint", p.Endpoint).Msg("Creating in-cluster Provider client") - client, err = newInClusterClient(p.Endpoint) - case os.Getenv("KUBECONFIG") != "": - logger.Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG")) - client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG")) - default: - logger.Info().Str("endpoint", p.Endpoint).Msg("Creating cluster-external Provider client") - client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token) - } - - if err != nil { - return nil, err - } - - client.labelSelector = p.LabelSelector - client.experimentalChannel = p.ExperimentalChannel - - return client, nil -} - // Init the provider. func (p *Provider) Init() error { logger := log.With().Str(logs.ProviderName, providerName).Logger() @@ -290,6 +247,49 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } +func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, route *gatev1.HTTPRoute) { + if p.routerTransform == nil { + return + } + + if err := p.routerTransform.Apply(ctx, rt, route); err != nil { + log.Ctx(ctx).Error().Err(err).Msg("Apply router transform") + } +} + +func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { + // Label selector validation + _, err := labels.Parse(p.LabelSelector) + if err != nil { + return nil, fmt.Errorf("invalid label selector: %q", p.LabelSelector) + } + + logger := log.Ctx(ctx) + logger.Info().Msgf("Label selector is: %q", p.LabelSelector) + + var client *clientWrapper + switch { + case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "": + logger.Info().Str("endpoint", p.Endpoint).Msg("Creating in-cluster Provider client") + client, err = newInClusterClient(p.Endpoint) + case os.Getenv("KUBECONFIG") != "": + logger.Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG")) + client, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG")) + default: + logger.Info().Str("endpoint", p.Endpoint).Msg("Creating cluster-external Provider client") + client, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token) + } + + if err != nil { + return nil, err + } + + client.labelSelector = p.LabelSelector + client.experimentalChannel = p.ExperimentalChannel + + return client, nil +} + // TODO Handle errors and update resources statuses (gatewayClass, gateway). func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.Configuration { conf := &dynamic.Configuration{ @@ -1227,12 +1227,12 @@ func getCertificateBlocks(secret *corev1.Secret, namespace, secretName string) ( return cert, key, nil } -func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} { +func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan any) chan any { if throttleDuration == 0 { return nil } // Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling) - eventsChanBuffered := make(chan interface{}, 1) + eventsChanBuffered := make(chan any, 1) // Run a goroutine that reads events from eventChan and does a non-blocking write to pendingEvent. // This guarantees that writing to eventChan will never block, diff --git a/pkg/provider/kubernetes/gateway/tcproute.go b/pkg/provider/kubernetes/gateway/tcproute.go index 74a741909..04c84af5d 100644 --- a/pkg/provider/kubernetes/gateway/tcproute.go +++ b/pkg/provider/kubernetes/gateway/tcproute.go @@ -3,6 +3,7 @@ package gateway import ( "context" "fmt" + "maps" "net" "strconv" "strings" @@ -318,21 +319,15 @@ func mergeTCPConfiguration(from, to *dynamic.Configuration) { if to.TCP.Routers == nil { to.TCP.Routers = map[string]*dynamic.TCPRouter{} } - for routerName, router := range from.TCP.Routers { - to.TCP.Routers[routerName] = router - } + maps.Copy(to.TCP.Routers, from.TCP.Routers) if to.TCP.Middlewares == nil { to.TCP.Middlewares = map[string]*dynamic.TCPMiddleware{} } - for middlewareName, middleware := range from.TCP.Middlewares { - to.TCP.Middlewares[middlewareName] = middleware - } + maps.Copy(to.TCP.Middlewares, from.TCP.Middlewares) if to.TCP.Services == nil { to.TCP.Services = map[string]*dynamic.TCPService{} } - for serviceName, service := range from.TCP.Services { - to.TCP.Services[serviceName] = service - } + maps.Copy(to.TCP.Services, from.TCP.Services) } diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go index 6536abed5..a9226729c 100644 --- a/pkg/provider/kubernetes/ingress-nginx/annotations.go +++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go @@ -54,7 +54,7 @@ type ingressConfig struct { // parseIngressConfig parses the annotations from an Ingress object into an ingressConfig struct. func parseIngressConfig(ing *netv1.Ingress) (ingressConfig, error) { cfg := ingressConfig{} - cfgType := reflect.TypeOf(cfg) + cfgType := reflect.TypeFor[ingressConfig]() cfgValue := reflect.ValueOf(&cfg).Elem() for i := range cfgType.NumField() { @@ -86,8 +86,7 @@ func parseIngressConfig(ing *netv1.Ingress) (ingressConfig, error) { if field.Type.Elem().Elem().Kind() == reflect.String { // Handle slice of strings var slice []string - elements := strings.Split(val, ",") - for _, elt := range elements { + for elt := range strings.SplitSeq(val, ",") { slice = append(slice, strings.TrimSpace(elt)) } cfgValue.Field(i).Set(reflect.ValueOf(&slice)) diff --git a/pkg/provider/kubernetes/ingress-nginx/client.go b/pkg/provider/kubernetes/ingress-nginx/client.go index 93eae4a09..9851c8860 100644 --- a/pkg/provider/kubernetes/ingress-nginx/client.go +++ b/pkg/provider/kubernetes/ingress-nginx/client.go @@ -123,9 +123,9 @@ func newClient(clientSet kclientset.Interface) *clientWrapper { } // WatchAll starts namespace-specific controllers for all relevant kinds. -func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelector string) (<-chan interface{}, error) { +func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelector string) (<-chan any, error) { stopCh := ctx.Done() - eventCh := make(chan interface{}, 1) + eventCh := make(chan any, 1) eventHandler := &k8s.ResourceEventHandler{Ev: eventCh} c.ignoreIngressClasses = false diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go index 268db6ef7..aff6f8bee 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go @@ -1039,8 +1039,7 @@ func basicAuthUsers(secret *corev1.Secret, authSecretType string) (dynamic.Users } // Trim lines and filter out blanks - rawLines := strings.Split(string(authFileContent), "\n") - for _, rawLine := range rawLines { + for rawLine := range strings.SplitSeq(string(authFileContent), "\n") { line := strings.TrimSpace(rawLine) if line != "" && !strings.HasPrefix(line, "#") { users = append(users, line) @@ -1101,13 +1100,13 @@ func buildPrefixRule(path string) string { return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", path) } -func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} { +func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan any) chan any { if throttleDuration == 0 { return nil } // Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling). - eventsChanBuffered := make(chan interface{}, 1) + eventsChanBuffered := make(chan any, 1) // Run a goroutine that reads events from eventChan and does a // non-blocking write to pendingEvent. This guarantees that writing to diff --git a/pkg/provider/kubernetes/ingress/client.go b/pkg/provider/kubernetes/ingress/client.go index 00b5397da..9645409ac 100644 --- a/pkg/provider/kubernetes/ingress/client.go +++ b/pkg/provider/kubernetes/ingress/client.go @@ -36,7 +36,7 @@ const ( // WatchAll starts the watch of the Provider resources and updates the stores. // The stores can then be accessed via the Get* functions. type Client interface { - WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) + WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan any, error) GetIngresses() []*netv1.Ingress GetIngressClasses() ([]*netv1.IngressClass, error) GetService(namespace, name string) (*corev1.Service, bool, error) @@ -138,8 +138,8 @@ func newClientImpl(clientset kclientset.Interface) *clientWrapper { } // WatchAll starts namespace-specific controllers for all relevant kinds. -func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) { - eventCh := make(chan interface{}, 1) +func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan any, error) { + eventCh := make(chan any, 1) eventHandler := &k8s.ResourceEventHandler{Ev: eventCh} if len(namespaces) == 0 { diff --git a/pkg/provider/kubernetes/ingress/client_mock_test.go b/pkg/provider/kubernetes/ingress/client_mock_test.go index 28b0a3501..c331c263c 100644 --- a/pkg/provider/kubernetes/ingress/client_mock_test.go +++ b/pkg/provider/kubernetes/ingress/client_mock_test.go @@ -26,7 +26,7 @@ type clientMock struct { apiNodesError error apiIngressStatusError error - watchChan chan interface{} + watchChan chan any } func newClientMock(path string) clientMock { @@ -117,7 +117,7 @@ func (c clientMock) GetIngressClasses() ([]*netv1.IngressClass, error) { return c.ingressClasses, nil } -func (c clientMock) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) { +func (c clientMock) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan any, error) { return c.watchChan, nil } diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 55d2ec6db..714aebdfb 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -70,62 +70,6 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) { p.routerTransform = routerTransform } -func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, ingress *netv1.Ingress) { - if p.routerTransform == nil { - return - } - - err := p.routerTransform.Apply(ctx, rt, ingress) - if err != nil { - log.Ctx(ctx).Error().Err(err).Msg("Apply router transform") - } -} - -// EndpointIngress holds the endpoint information for the Kubernetes provider. -type EndpointIngress struct { - IP string `description:"IP used for Kubernetes Ingress endpoints." json:"ip,omitempty" toml:"ip,omitempty" yaml:"ip,omitempty"` - Hostname string `description:"Hostname used for Kubernetes Ingress endpoints." json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"` - PublishedService string `description:"Published Kubernetes Service to copy status from." json:"publishedService,omitempty" toml:"publishedService,omitempty" yaml:"publishedService,omitempty"` -} - -func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { - _, err := labels.Parse(p.LabelSelector) - if err != nil { - return nil, fmt.Errorf("invalid ingress label selector: %q", p.LabelSelector) - } - - logger := log.Ctx(ctx) - - logger.Info().Msgf("ingress label selector is: %q", p.LabelSelector) - - withEndpoint := "" - if p.Endpoint != "" { - withEndpoint = fmt.Sprintf(" with endpoint %v", p.Endpoint) - } - - var cl *clientWrapper - switch { - case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "": - logger.Info().Msgf("Creating in-cluster Provider client%s", withEndpoint) - cl, err = newInClusterClient(p.Endpoint) - case os.Getenv("KUBECONFIG") != "": - logger.Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG")) - cl, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG")) - default: - logger.Info().Msgf("Creating cluster-external Provider client%s", withEndpoint) - cl, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token) - } - - if err != nil { - return nil, err - } - - cl.ingressLabelSelector = p.LabelSelector - cl.disableIngressClassInformer = p.DisableIngressClassLookup || p.DisableClusterScopeResources - cl.disableClusterScopeInformer = p.DisableClusterScopeResources - return cl, nil -} - // Init the provider. func (p *Provider) Init() error { return nil @@ -213,6 +157,62 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } +func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router, ingress *netv1.Ingress) { + if p.routerTransform == nil { + return + } + + err := p.routerTransform.Apply(ctx, rt, ingress) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("Apply router transform") + } +} + +// EndpointIngress holds the endpoint information for the Kubernetes provider. +type EndpointIngress struct { + IP string `description:"IP used for Kubernetes Ingress endpoints." json:"ip,omitempty" toml:"ip,omitempty" yaml:"ip,omitempty"` + Hostname string `description:"Hostname used for Kubernetes Ingress endpoints." json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"` + PublishedService string `description:"Published Kubernetes Service to copy status from." json:"publishedService,omitempty" toml:"publishedService,omitempty" yaml:"publishedService,omitempty"` +} + +func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { + _, err := labels.Parse(p.LabelSelector) + if err != nil { + return nil, fmt.Errorf("invalid ingress label selector: %q", p.LabelSelector) + } + + logger := log.Ctx(ctx) + + logger.Info().Msgf("ingress label selector is: %q", p.LabelSelector) + + withEndpoint := "" + if p.Endpoint != "" { + withEndpoint = fmt.Sprintf(" with endpoint %v", p.Endpoint) + } + + var cl *clientWrapper + switch { + case os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "": + logger.Info().Msgf("Creating in-cluster Provider client%s", withEndpoint) + cl, err = newInClusterClient(p.Endpoint) + case os.Getenv("KUBECONFIG") != "": + logger.Info().Msgf("Creating cluster-external Provider client from KUBECONFIG %s", os.Getenv("KUBECONFIG")) + cl, err = newExternalClusterClientFromFile(os.Getenv("KUBECONFIG")) + default: + logger.Info().Msgf("Creating cluster-external Provider client%s", withEndpoint) + cl, err = newExternalClusterClient(p.Endpoint, p.CertAuthFilePath, p.Token) + } + + if err != nil { + return nil, err + } + + cl.ingressLabelSelector = p.LabelSelector + cl.disableIngressClassInformer = p.DisableIngressClassLookup || p.DisableClusterScopeResources + cl.disableClusterScopeInformer = p.DisableClusterScopeResources + return cl, nil +} + func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Client) *dynamic.Configuration { conf := &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ @@ -901,13 +901,13 @@ func buildStrictPrefixMatchingRule(path string) string { return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", path) } -func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} { +func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan any) chan any { if throttleDuration == 0 { return nil } // Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling). - eventsChanBuffered := make(chan interface{}, 1) + eventsChanBuffered := make(chan any, 1) // Run a goroutine that reads events from eventChan and does a // non-blocking write to pendingEvent. This guarantees that writing to diff --git a/pkg/provider/kubernetes/k8s/event_handler.go b/pkg/provider/kubernetes/k8s/event_handler.go index 9b3babaa9..6097df1b9 100644 --- a/pkg/provider/kubernetes/k8s/event_handler.go +++ b/pkg/provider/kubernetes/k8s/event_handler.go @@ -7,38 +7,38 @@ import ( // ResourceEventHandler handles Add, Update or Delete Events for resources. type ResourceEventHandler struct { - Ev chan<- interface{} + Ev chan<- any } // OnAdd is called on Add Events. -func (reh *ResourceEventHandler) OnAdd(obj interface{}, _ bool) { +func (reh *ResourceEventHandler) OnAdd(obj any, _ bool) { eventHandlerFunc(reh.Ev, obj) } // OnUpdate is called on Update Events. // Ignores useless changes. -func (reh *ResourceEventHandler) OnUpdate(oldObj, newObj interface{}) { +func (reh *ResourceEventHandler) OnUpdate(oldObj, newObj any) { if objChanged(oldObj, newObj) { eventHandlerFunc(reh.Ev, newObj) } } // OnDelete is called on Delete Events. -func (reh *ResourceEventHandler) OnDelete(obj interface{}) { +func (reh *ResourceEventHandler) OnDelete(obj any) { eventHandlerFunc(reh.Ev, obj) } // eventHandlerFunc will pass the obj on to the events channel or drop it. // This is so passing the events along won't block in the case of high volume. // The events are only used for signaling anyway so dropping a few is ok. -func eventHandlerFunc(events chan<- interface{}, obj interface{}) { +func eventHandlerFunc(events chan<- any, obj any) { select { case events <- obj: default: } } -func objChanged(oldObj, newObj interface{}) bool { +func objChanged(oldObj, newObj any) bool { if oldObj == nil || newObj == nil { return true } diff --git a/pkg/provider/kubernetes/k8s/event_handler_test.go b/pkg/provider/kubernetes/k8s/event_handler_test.go index bcde5cc48..256ec6c1f 100644 --- a/pkg/provider/kubernetes/k8s/event_handler_test.go +++ b/pkg/provider/kubernetes/k8s/event_handler_test.go @@ -14,8 +14,8 @@ func Test_detectChanges(t *testing.T) { portB := int32(8080) tests := []struct { name string - oldObj interface{} - newObj interface{} + oldObj any + newObj any want bool }{ { diff --git a/pkg/provider/kubernetes/knative/client.go b/pkg/provider/kubernetes/knative/client.go index bfb87c332..a56b646ad 100644 --- a/pkg/provider/kubernetes/knative/client.go +++ b/pkg/provider/kubernetes/knative/client.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "slices" "time" "github.com/rs/zerolog/log" @@ -108,8 +109,8 @@ func newExternalClusterClient(endpoint, token, caFilePath string) (*clientWrappe } // WatchAll starts namespace-specific controllers for all relevant kinds. -func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) { - eventCh := make(chan interface{}, 1) +func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan any, error) { + eventCh := make(chan any, 1) eventHandler := &k8s.ResourceEventHandler{Ev: eventCh} if len(namespaces) == 0 { @@ -210,12 +211,7 @@ func (c *clientWrapper) isWatchedNamespace(ns string) bool { if c.isNamespaceAll { return true } - for _, watchedNamespace := range c.watchedNamespaces { - if watchedNamespace == ns { - return true - } - } - return false + return slices.Contains(c.watchedNamespaces, ns) } // lookupNamespace returns the lookup namespace key for the given namespace. diff --git a/pkg/provider/kubernetes/knative/kubernetes.go b/pkg/provider/kubernetes/knative/kubernetes.go index ae9c636d3..a7842a6e0 100644 --- a/pkg/provider/kubernetes/knative/kubernetes.go +++ b/pkg/provider/kubernetes/knative/kubernetes.go @@ -338,9 +338,7 @@ func (p *Provider) buildRouters(ctx context.Context, ingress *knativenetworkingv } conf.Services[routerKey+"-wrr"] = &dynamic.Service{Weighted: wrr} - for k, v := range services { - conf.Services[k] = v - } + maps.Copy(conf.Services, services) } } @@ -485,27 +483,21 @@ func mergeHTTPConfigs(confs ...*dynamic.HTTPConfiguration) *dynamic.HTTPConfigur } for _, c := range confs { - for k, v := range c.Routers { - conf.Routers[k] = v - } - for k, v := range c.Middlewares { - conf.Middlewares[k] = v - } - for k, v := range c.Services { - conf.Services[k] = v - } + maps.Copy(conf.Routers, c.Routers) + maps.Copy(conf.Middlewares, c.Middlewares) + maps.Copy(conf.Services, c.Services) } return conf } -func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} { +func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan any) chan any { logger := log.Ctx(ctx).With().Logger() if throttleDuration == 0 { return nil } // Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling) - eventsChanBuffered := make(chan interface{}, 1) + eventsChanBuffered := make(chan any, 1) // Run a goroutine that reads events from eventChan and does a non-blocking write to pendingEvent. // This guarantees that writing to eventChan will never block, diff --git a/pkg/provider/nomad/nomad.go b/pkg/provider/nomad/nomad.go index c9581a036..ec3b8e2d8 100644 --- a/pkg/provider/nomad/nomad.go +++ b/pkg/provider/nomad/nomad.go @@ -263,6 +263,11 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return nil } +// Namespace returns the namespace of the Nomad provider. +func (p *Provider) Namespace() string { + return p.namespace +} + func (p *Provider) pollOrWatch(ctx context.Context) (<-chan *api.Events, error) { if p.Watch { return p.client.EventStream().Stream(ctx, @@ -571,8 +576,3 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *s return eventsChanBuffered } - -// Namespace returns the namespace of the Nomad provider. -func (p *Provider) Namespace() string { - return p.namespace -} diff --git a/pkg/proxy/fast/proxy.go b/pkg/proxy/fast/proxy.go index 76b44a1db..cf9b962b1 100644 --- a/pkg/proxy/fast/proxy.go +++ b/pkg/proxy/fast/proxy.go @@ -362,7 +362,7 @@ type fasthttpHeader interface { // See RFC 7230, section 6.1. func removeConnectionHeaders(h fasthttpHeader) { f := h.Peek(fasthttp.HeaderConnection) - for _, sf := range bytes.Split(f, []byte{','}) { + for sf := range bytes.SplitSeq(f, []byte{','}) { if sf = bytes.TrimSpace(sf); len(sf) > 0 { h.DelBytes(sf) } diff --git a/pkg/proxy/httputil/bufferpool.go b/pkg/proxy/httputil/bufferpool.go index 7fccd6bc4..2c3769c7b 100644 --- a/pkg/proxy/httputil/bufferpool.go +++ b/pkg/proxy/httputil/bufferpool.go @@ -13,7 +13,7 @@ func newBufferPool() *bufferPool { pool: sync.Pool{}, } - b.pool.New = func() interface{} { + b.pool.New = func() any { return make([]byte, bufferSize) } diff --git a/pkg/redactor/redactor.go b/pkg/redactor/redactor.go index cbe4a6c5c..c545d98d7 100644 --- a/pkg/redactor/redactor.go +++ b/pkg/redactor/redactor.go @@ -20,11 +20,11 @@ const ( // Anonymize redacts the configuration fields that do not have an export=true struct tag. // It returns the resulting marshaled configuration. -func Anonymize(baseConfig interface{}) (string, error) { +func Anonymize(baseConfig any) (string, error) { return anonymize(baseConfig, false) } -func anonymize(baseConfig interface{}, indent bool) (string, error) { +func anonymize(baseConfig any, indent bool) (string, error) { conf, err := do(baseConfig, tagExport, true, indent) if err != nil { return "", err @@ -34,17 +34,17 @@ func anonymize(baseConfig interface{}, indent bool) (string, error) { // RemoveCredentials redacts the configuration fields that have a loggable=false struct tag. // It returns the resulting marshaled configuration. -func RemoveCredentials(baseConfig interface{}) (string, error) { +func RemoveCredentials(baseConfig any) (string, error) { return removeCredentials(baseConfig, false) } -func removeCredentials(baseConfig interface{}, indent bool) (string, error) { +func removeCredentials(baseConfig any, indent bool) (string, error) { return do(baseConfig, tagLoggable, false, indent) } // do marshals the given configuration, while redacting some of the fields // respectively to the given tag. -func do(baseConfig interface{}, tag string, redactByDefault, indent bool) (string, error) { +func do(baseConfig any, tag string, redactByDefault, indent bool) (string, error) { anomConfig, err := copystructure.Copy(baseConfig) if err != nil { return "", err @@ -211,7 +211,7 @@ func isExported(f reflect.StructField) bool { return true } -func marshal(anomConfig interface{}, indent bool) ([]byte, error) { +func marshal(anomConfig any, indent bool) ([]byte, error) { if indent { return json.MarshalIndent(anomConfig, "", " ") } diff --git a/pkg/rules/parser.go b/pkg/rules/parser.go index 2555c01b9..e3adc53d6 100644 --- a/pkg/rules/parser.go +++ b/pkg/rules/parser.go @@ -2,6 +2,7 @@ package rules import ( "fmt" + "slices" "strings" "github.com/vulcand/predicate" @@ -28,7 +29,7 @@ type Tree struct { // NewParser constructs a parser for the given matchers. func NewParser(matchers []string) (predicate.Parser, error) { - parserFuncs := make(map[string]interface{}) + parserFuncs := make(map[string]any) for _, matcherName := range matchers { fn := func(value ...string) TreeBuilder { @@ -104,10 +105,8 @@ func (tree *Tree) ParseMatchers(matchers []string) []string { case and, or: return append(tree.RuleLeft.ParseMatchers(matchers), tree.RuleRight.ParseMatchers(matchers)...) default: - for _, matcher := range matchers { - if tree.Matcher == matcher { - return lower(tree.Value) - } + if slices.Contains(matchers, tree.Matcher) { + return lower(tree.Value) } return nil diff --git a/pkg/safe/routine.go b/pkg/safe/routine.go index ab6913c87..1c3abc631 100644 --- a/pkg/safe/routine.go +++ b/pkg/safe/routine.go @@ -49,7 +49,7 @@ func Go(goroutine func()) { } // GoWithRecover starts a recoverable goroutine using given customRecover() function. -func GoWithRecover(goroutine func(), customRecover func(err interface{})) { +func GoWithRecover(goroutine func(), customRecover func(err any)) { go func() { defer func() { if err := recover(); err != nil { @@ -60,7 +60,7 @@ func GoWithRecover(goroutine func(), customRecover func(err interface{})) { }() } -func defaultRecoverGoroutine(err interface{}) { +func defaultRecoverGoroutine(err any) { log.Error().Interface("error", err).Msg("Error in Go routine") log.Error().Msgf("Stack: %s", debug.Stack()) } diff --git a/pkg/safe/routine_test.go b/pkg/safe/routine_test.go index 3fa403e78..54c81bc1b 100644 --- a/pkg/safe/routine_test.go +++ b/pkg/safe/routine_test.go @@ -29,6 +29,7 @@ func TestNewPoolContext(t *testing.T) { type fakeRoutine struct { sync.Mutex + started bool startSig chan bool } diff --git a/pkg/safe/safe.go b/pkg/safe/safe.go index 8b44beb3f..822006cb8 100644 --- a/pkg/safe/safe.go +++ b/pkg/safe/safe.go @@ -6,24 +6,24 @@ import ( // Safe contains a thread-safe value. type Safe struct { - value interface{} + value any lock sync.RWMutex } // New create a new Safe instance given a value. -func New(value interface{}) *Safe { +func New(value any) *Safe { return &Safe{value: value, lock: sync.RWMutex{}} } // Get returns the value. -func (s *Safe) Get() interface{} { +func (s *Safe) Get() any { s.lock.RLock() defer s.lock.RUnlock() return s.value } // Set sets a new value. -func (s *Safe) Set(value interface{}) { +func (s *Safe) Set(value any) { s.lock.Lock() defer s.lock.Unlock() s.value = value diff --git a/pkg/server/middleware/plugins.go b/pkg/server/middleware/plugins.go index 0529ca190..109f559d4 100644 --- a/pkg/server/middleware/plugins.go +++ b/pkg/server/middleware/plugins.go @@ -13,16 +13,16 @@ const typeName = "Plugin" // PluginsBuilder the plugin's builder interface. type PluginsBuilder interface { - Build(pName string, config map[string]interface{}, middlewareName string) (plugins.Constructor, error) + Build(pName string, config map[string]any, middlewareName string) (plugins.Constructor, error) } -func findPluginConfig(rawConfig map[string]dynamic.PluginConf) (string, map[string]interface{}, error) { +func findPluginConfig(rawConfig map[string]dynamic.PluginConf) (string, map[string]any, error) { if len(rawConfig) != 1 { return "", nil, errors.New("invalid configuration: no configuration or too many plugin definition") } var pluginType string - var rawPluginConfig map[string]interface{} + var rawPluginConfig map[string]any for pType, pConfig := range rawConfig { pluginType = pType diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index a1bd011a0..fc9190e4b 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -67,12 +67,96 @@ func NewManager(conf *runtime.Configuration, } } -func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo { - if m.conf != nil { - return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls) +// ParseRouterTree sets up router tree and validates router configuration. +// This function performs the following operations in order: +// +// 1. Populate ChildRefs: Uses ParentRefs to build the parent-child relationship graph +// 2. Root-first traversal: Starting from root routers (no ParentRefs), traverses the tree +// 3. Cycle detection: Detects circular dependencies and removes cyclic links +// 4. Reachability check: Marks routers unreachable from any root as disabled +// 5. Dead-end detection: Marks routers with no service and no children as disabled +// 6. Validation: Checks for configuration errors +// +// Router status is set during this process: +// - Enabled: Reachable routers with valid configuration +// - Disabled: Unreachable, dead-end, or routers with critical errors +// - Warning: Routers with non-critical errors (like cycles) +// +// The function modifies router.Status, router.ChildRefs, and adds errors to router.Err. +func (m *Manager) ParseRouterTree() { + if m.conf == nil || m.conf.Routers == nil { + return } - return make(map[string]map[string]*runtime.RouterInfo) + // Populate ChildRefs based on ParentRefs and find root routers. + var rootRouters []string + for routerName, router := range m.conf.Routers { + if len(router.ParentRefs) == 0 { + rootRouters = append(rootRouters, routerName) + continue + } + + for _, parentName := range router.ParentRefs { + if parentRouter, exists := m.conf.Routers[parentName]; exists { + // Add this router as a child of its parent + if !slices.Contains(parentRouter.ChildRefs, routerName) { + parentRouter.ChildRefs = append(parentRouter.ChildRefs, routerName) + } + } else { + router.AddError(fmt.Errorf("parent router %q does not exist", parentName), true) + } + } + + // Check for non-root router with TLS config. + if router.TLS != nil { + router.AddError(errors.New("non-root router cannot have TLS configuration"), true) + continue + } + + // Check for non-root router with Observability config. + if router.Observability != nil { + router.AddError(errors.New("non-root router cannot have Observability configuration"), true) + continue + } + + // Check for non-root router with Entrypoint config. + if len(router.EntryPoints) > 0 { + router.AddError(errors.New("non-root router cannot have Entrypoints configuration"), true) + continue + } + } + sort.Strings(rootRouters) + + // Root-first traversal with cycle detection. + visited := make(map[string]bool) + currentPath := make(map[string]bool) + var path []string + + for _, rootName := range rootRouters { + if !visited[rootName] { + m.traverse(rootName, visited, currentPath, path) + } + } + + for routerName, router := range m.conf.Routers { + // Set status for all routers based on reachability. + if !visited[routerName] { + router.AddError(errors.New("router is not reachable"), true) + continue + } + + // Detect dead-end routers (no service + no children) - AFTER cycle handling. + if router.Service == "" && len(router.ChildRefs) == 0 { + router.AddError(errors.New("router has no service and no child routers"), true) + continue + } + + // Check for router with service that is referenced as a parent. + if router.Service != "" && len(router.ChildRefs) > 0 { + router.AddError(errors.New("router has both a service and is referenced as a parent by other routers"), true) + continue + } + } } // BuildHandlers Builds handler for all entry points. @@ -132,6 +216,14 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t return entryPointHandlers } +func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo { + if m.conf != nil { + return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls) + } + + return make(map[string]map[string]*runtime.RouterInfo) +} + func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo, config dynamic.RouterObservabilityConfig) (http.Handler, error) { muxer := httpmuxer.NewMuxer(m.parser) @@ -285,98 +377,6 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn return chain.Extend(*mHandler).Then(nextHandler) } -// ParseRouterTree sets up router tree and validates router configuration. -// This function performs the following operations in order: -// -// 1. Populate ChildRefs: Uses ParentRefs to build the parent-child relationship graph -// 2. Root-first traversal: Starting from root routers (no ParentRefs), traverses the tree -// 3. Cycle detection: Detects circular dependencies and removes cyclic links -// 4. Reachability check: Marks routers unreachable from any root as disabled -// 5. Dead-end detection: Marks routers with no service and no children as disabled -// 6. Validation: Checks for configuration errors -// -// Router status is set during this process: -// - Enabled: Reachable routers with valid configuration -// - Disabled: Unreachable, dead-end, or routers with critical errors -// - Warning: Routers with non-critical errors (like cycles) -// -// The function modifies router.Status, router.ChildRefs, and adds errors to router.Err. -func (m *Manager) ParseRouterTree() { - if m.conf == nil || m.conf.Routers == nil { - return - } - - // Populate ChildRefs based on ParentRefs and find root routers. - var rootRouters []string - for routerName, router := range m.conf.Routers { - if len(router.ParentRefs) == 0 { - rootRouters = append(rootRouters, routerName) - continue - } - - for _, parentName := range router.ParentRefs { - if parentRouter, exists := m.conf.Routers[parentName]; exists { - // Add this router as a child of its parent - if !slices.Contains(parentRouter.ChildRefs, routerName) { - parentRouter.ChildRefs = append(parentRouter.ChildRefs, routerName) - } - } else { - router.AddError(fmt.Errorf("parent router %q does not exist", parentName), true) - } - } - - // Check for non-root router with TLS config. - if router.TLS != nil { - router.AddError(errors.New("non-root router cannot have TLS configuration"), true) - continue - } - - // Check for non-root router with Observability config. - if router.Observability != nil { - router.AddError(errors.New("non-root router cannot have Observability configuration"), true) - continue - } - - // Check for non-root router with Entrypoint config. - if len(router.EntryPoints) > 0 { - router.AddError(errors.New("non-root router cannot have Entrypoints configuration"), true) - continue - } - } - sort.Strings(rootRouters) - - // Root-first traversal with cycle detection. - visited := make(map[string]bool) - currentPath := make(map[string]bool) - var path []string - - for _, rootName := range rootRouters { - if !visited[rootName] { - m.traverse(rootName, visited, currentPath, path) - } - } - - for routerName, router := range m.conf.Routers { - // Set status for all routers based on reachability. - if !visited[routerName] { - router.AddError(errors.New("router is not reachable"), true) - continue - } - - // Detect dead-end routers (no service + no children) - AFTER cycle handling. - if router.Service == "" && len(router.ChildRefs) == 0 { - router.AddError(errors.New("router has no service and no child routers"), true) - continue - } - - // Check for router with service that is referenced as a parent. - if router.Service != "" && len(router.ChildRefs) > 0 { - router.AddError(errors.New("router has both a service and is referenced as a parent by other routers"), true) - continue - } - } -} - // traverse performs a depth-first traversal starting from root routers, // detecting cycles and marking visited routers for reachability detection. func (m *Manager) traverse(routerName string, visited, currentPath map[string]bool, path []string) { diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index a10c597b7..eb334d183 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -865,7 +865,7 @@ func BenchmarkRouterServe(b *testing.B) { reqHost := requestdecorator.New(nil) b.ReportAllocs() - for range b.N { + for b.Loop() { reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP) } } @@ -900,7 +900,7 @@ func BenchmarkService(b *testing.B) { handler, _ := serviceManager.BuildHTTP(b.Context(), "foo-service") b.ReportAllocs() - for range b.N { + for b.Loop() { handler.ServeHTTP(w, req) } } diff --git a/pkg/server/router/tcp/manager.go b/pkg/server/router/tcp/manager.go index 788bf7cb0..c266e84d5 100644 --- a/pkg/server/router/tcp/manager.go +++ b/pkg/server/router/tcp/manager.go @@ -27,6 +27,16 @@ type middlewareBuilder interface { BuildChain(ctx context.Context, names []string) *tcp.Chain } +// Manager is a route/router manager. +type Manager struct { + serviceManager *tcpservice.Manager + middlewaresBuilder middlewareBuilder + httpHandlers map[string]http.Handler + httpsHandlers map[string]http.Handler + tlsManager *traefiktls.Manager + conf *runtime.Configuration +} + // NewManager Creates a new Manager. func NewManager(conf *runtime.Configuration, serviceManager *tcpservice.Manager, @@ -45,32 +55,6 @@ func NewManager(conf *runtime.Configuration, } } -// Manager is a route/router manager. -type Manager struct { - serviceManager *tcpservice.Manager - middlewaresBuilder middlewareBuilder - httpHandlers map[string]http.Handler - httpsHandlers map[string]http.Handler - tlsManager *traefiktls.Manager - conf *runtime.Configuration -} - -func (m *Manager) getTCPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.TCPRouterInfo { - if m.conf != nil { - return m.conf.GetTCPRoutersByEntryPoints(ctx, entryPoints) - } - - return make(map[string]map[string]*runtime.TCPRouterInfo) -} - -func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo { - if m.conf != nil { - return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls) - } - - return make(map[string]map[string]*runtime.RouterInfo) -} - // BuildHandlers builds the handlers for the given entrypoints. func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]*Router { entryPointsRouters := m.getTCPRouters(rootCtx, entryPoints) @@ -93,6 +77,22 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m return entryPointHandlers } +func (m *Manager) getTCPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.TCPRouterInfo { + if m.conf != nil { + return m.conf.GetTCPRoutersByEntryPoints(ctx, entryPoints) + } + + return make(map[string]map[string]*runtime.TCPRouterInfo) +} + +func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo { + if m.conf != nil { + return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls) + } + + return make(map[string]map[string]*runtime.RouterInfo) +} + type nameAndConfig struct { routerName string // just so we have it as additional information when logging TLSConfig *tls.Config diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 283f1364c..b2b4ec93f 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -216,27 +216,6 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { conn.Close() } -// acmeTLSALPNHandler returns a special handler to solve ACME-TLS/1 challenges. -func (r *Router) acmeTLSALPNHandler() tcp.Handler { - if r.httpsTLSConfig == nil { - return &brokenTLSRouter{} - } - - return tcp.HandlerFunc(func(conn tcp.WriteCloser) { - tlsConn := tls.Server(conn, r.httpsTLSConfig) - defer tlsConn.Close() - - // This avoids stale connections when validating the ACME challenge, - // as we expect a validation request to complete in a short period of time. - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - if err := tlsConn.HandshakeContext(ctx); err != nil { - log.Debug().Err(err).Msg("Error during ACME-TLS/1 handshake") - } - }) -} - // AddTCPRoute defines a handler for the given rule. func (r *Router) AddTCPRoute(rule string, priority int, target tcp.Handler) error { return r.muxerTCP.AddRoute(rule, "", priority, target) @@ -277,16 +256,6 @@ func (r *Router) SetHTTPForwarder(handler tcp.Handler) { r.httpForwarder = handler } -// brokenTLSRouter is associated to a Host(SNI) rule for which we know the TLS conf is broken. -// It is used to make sure any attempt to connect to that hostname is closed, -// since we cannot proceed with the intended TLS conf. -type brokenTLSRouter struct{} - -// ServeTCP instantly closes the connection. -func (t *brokenTLSRouter) ServeTCP(conn tcp.WriteCloser) { - _ = conn.Close() -} - // SetHTTPSForwarder sets the tcp handler that will forward the TLS connections to an HTTP handler. // It also sets up each TLS handler (with its TLS config) for each Host(SNI) rule we previously kept track of. // It sets up a special handler that closes the connection if a TLS config is nil. @@ -334,17 +303,48 @@ func (r *Router) EnableACMETLSPassthrough() { r.acmeTLSPassthrough = true } +// acmeTLSALPNHandler returns a special handler to solve ACME-TLS/1 challenges. +func (r *Router) acmeTLSALPNHandler() tcp.Handler { + if r.httpsTLSConfig == nil { + return &brokenTLSRouter{} + } + + return tcp.HandlerFunc(func(conn tcp.WriteCloser) { + tlsConn := tls.Server(conn, r.httpsTLSConfig) + defer tlsConn.Close() + + // This avoids stale connections when validating the ACME challenge, + // as we expect a validation request to complete in a short period of time. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + if err := tlsConn.HandshakeContext(ctx); err != nil { + log.Debug().Err(err).Msg("Error during ACME-TLS/1 handshake") + } + }) +} + +// brokenTLSRouter is associated to a Host(SNI) rule for which we know the TLS conf is broken. +// It is used to make sure any attempt to connect to that hostname is closed, +// since we cannot proceed with the intended TLS conf. +type brokenTLSRouter struct{} + +// ServeTCP instantly closes the connection. +func (t *brokenTLSRouter) ServeTCP(conn tcp.WriteCloser) { + _ = conn.Close() +} + // Conn is a connection proxy that handles Peeked bytes. type Conn struct { - // Peeked are the bytes that have been read from Conn for the purposes of route matching, - // but have not yet been consumed by Read calls. - // It set to nil by Read when fully consumed. - Peeked []byte - // Conn is the underlying connection. // It can be type asserted against *net.TCPConn or other types as needed. // It should not be read from directly unless Peeked is nil. tcp.WriteCloser + + // Peeked are the bytes that have been read from Conn for the purposes of route matching, + // but have not yet been consumed by Read calls. + // It set to nil by Read when fully consumed. + Peeked []byte } // Read reads bytes from the connection (using the buffer prior to actually reading). @@ -453,8 +453,9 @@ func getPeeked(br *bufio.Reader) string { // helloSniffConn is a net.Conn that reads from r, fails on Writes, // and crashes otherwise. type helloSniffConn struct { - r io.Reader net.Conn // nil; crash on any unexpected use + + r io.Reader } // Read reads from the underlying reader. diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 768a78ea9..12bb4a378 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -32,6 +32,7 @@ type checkRouter func(addr string, timeout time.Duration) error type httpForwarder struct { net.Listener + connChan chan net.Conn errChan chan error } @@ -1112,6 +1113,11 @@ func TestPostgres(t *testing.T) { require.Equal(t, []byte("OK"), b) } +type MockConn struct { + dataRead chan []byte + dataWrite chan []byte +} + func NewMockConn() *MockConn { return &MockConn{ dataRead: make(chan []byte), @@ -1119,11 +1125,6 @@ func NewMockConn() *MockConn { } } -type MockConn struct { - dataRead chan []byte - dataWrite chan []byte -} - func (m *MockConn) Read(b []byte) (n int, err error) { temp := <-m.dataRead copy(b, temp) diff --git a/pkg/server/router/udp/router.go b/pkg/server/router/udp/router.go index 910efa499..51b7bc669 100644 --- a/pkg/server/router/udp/router.go +++ b/pkg/server/router/udp/router.go @@ -13,6 +13,12 @@ import ( "github.com/traefik/traefik/v3/pkg/udp" ) +// Manager is a route/router manager. +type Manager struct { + serviceManager *udpservice.Manager + conf *runtime.Configuration +} + // NewManager Creates a new Manager. func NewManager(conf *runtime.Configuration, serviceManager *udpservice.Manager, @@ -23,20 +29,6 @@ func NewManager(conf *runtime.Configuration, } } -// Manager is a route/router manager. -type Manager struct { - serviceManager *udpservice.Manager - conf *runtime.Configuration -} - -func (m *Manager) getUDPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.UDPRouterInfo { - if m.conf != nil { - return m.conf.GetUDPRoutersByEntryPoints(ctx, entryPoints) - } - - return make(map[string]map[string]*runtime.UDPRouterInfo) -} - // BuildHandlers builds the handlers for the given entrypoints. func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]udp.Handler { entryPointsRouters := m.getUDPRouters(rootCtx, entryPoints) @@ -62,6 +54,14 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m return entryPointHandlers } +func (m *Manager) getUDPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.UDPRouterInfo { + if m.conf != nil { + return m.conf.GetUDPRoutersByEntryPoints(ctx, entryPoints) + } + + return make(map[string]map[string]*runtime.UDPRouterInfo) +} + func (m *Manager) buildEntryPointHandlers(ctx context.Context, configs map[string]*runtime.UDPRouterInfo) []udp.Handler { var rtNames []string for routerName := range configs { diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 71f5fc2b3..16d03bb84 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -398,6 +398,7 @@ func (e *TCPEntryPoint) SwitchRouter(rt *tcprouter.Router) { // connection type that was found to satisfy WriteCloser. type writeCloserWrapper struct { net.Conn + writeCloser tcp.WriteCloser } @@ -566,23 +567,6 @@ func (c *connectionTracker) RemoveConnection(conn net.Conn) { c.connsMu.Unlock() } -// syncOpenConnectionGauge updates openConnectionsGauge value with the conns map length. -func (c *connectionTracker) syncOpenConnectionGauge() { - if c.openConnectionsGauge == nil { - return - } - - c.connsMu.RLock() - c.openConnectionsGauge.Set(float64(len(c.conns))) - c.connsMu.RUnlock() -} - -func (c *connectionTracker) isEmpty() bool { - c.connsMu.RLock() - defer c.connsMu.RUnlock() - return len(c.conns) == 0 -} - // Shutdown wait for the connection closing. func (c *connectionTracker) Shutdown(ctx context.Context) error { ticker := time.NewTicker(500 * time.Millisecond) @@ -611,6 +595,23 @@ func (c *connectionTracker) Close() { } } +// syncOpenConnectionGauge updates openConnectionsGauge value with the conns map length. +func (c *connectionTracker) syncOpenConnectionGauge() { + if c.openConnectionsGauge == nil { + return + } + + c.connsMu.RLock() + c.openConnectionsGauge.Set(float64(len(c.conns))) + c.connsMu.RUnlock() +} + +func (c *connectionTracker) isEmpty() bool { + c.connsMu.RLock() + defer c.connsMu.RUnlock() + return len(c.conns) == 0 +} + type stoppable interface { Shutdown(ctx context.Context) error Close() error @@ -758,8 +759,9 @@ func newTrackedConnection(conn tcp.WriteCloser, tracker *connectionTracker) *tra } type trackedConnection struct { - tracker *connectionTracker tcp.WriteCloser + + tracker *connectionTracker } func (t *trackedConnection) Close() error { diff --git a/pkg/server/server_entrypoint_tcp_http3.go b/pkg/server/server_entrypoint_tcp_http3.go index 59be11262..7a2b0cadf 100644 --- a/pkg/server/server_entrypoint_tcp_http3.go +++ b/pkg/server/server_entrypoint_tcp_http3.go @@ -94,14 +94,14 @@ func (e *http3server) Switch(rt *tcprouter.Router) { e.getter = rt.GetTLSGetClientInfo() } +func (e *http3server) Shutdown(_ context.Context) error { + // TODO: use e.Server.CloseGracefully() when available. + return e.Server.Close() +} + func (e *http3server) getGetConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) { e.lock.RLock() defer e.lock.RUnlock() return e.getter(info) } - -func (e *http3server) Shutdown(_ context.Context) error { - // TODO: use e.Server.CloseGracefully() when available. - return e.Server.Close() -} diff --git a/pkg/server/server_signals.go b/pkg/server/server_signals.go index c582fce5c..be86925e7 100644 --- a/pkg/server/server_signals.go +++ b/pkg/server/server_signals.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package server diff --git a/pkg/server/server_signals_windows.go b/pkg/server/server_signals_windows.go index 8bfd91fcf..e2566f020 100644 --- a/pkg/server/server_signals_windows.go +++ b/pkg/server/server_signals_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package server diff --git a/pkg/server/service/loadbalancer/failover/failover_test.go b/pkg/server/service/loadbalancer/failover/failover_test.go index 3d357017e..8ca46e251 100644 --- a/pkg/server/service/loadbalancer/failover/failover_test.go +++ b/pkg/server/service/loadbalancer/failover/failover_test.go @@ -12,6 +12,7 @@ import ( type responseRecorder struct { *httptest.ResponseRecorder + save map[string]int sequence []string status []int diff --git a/pkg/server/service/loadbalancer/hrw/hrw.go b/pkg/server/service/loadbalancer/hrw/hrw.go index 51ca397fd..c011d3b23 100644 --- a/pkg/server/service/loadbalancer/hrw/hrw.go +++ b/pkg/server/service/loadbalancer/hrw/hrw.go @@ -17,6 +17,7 @@ var errNoAvailableServer = errors.New("no available server") type namedHandler struct { http.Handler + name string weight float64 } @@ -123,37 +124,6 @@ func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error { return nil } -func (b *Balancer) nextServer(ip string) (*namedHandler, error) { - b.handlersMu.RLock() - var healthy []*namedHandler - for _, h := range b.handlers { - if _, ok := b.status[h.name]; ok { - if _, fenced := b.fenced[h.name]; !fenced { - healthy = append(healthy, h) - } - } - } - b.handlersMu.RUnlock() - - if len(healthy) == 0 { - return nil, errNoAvailableServer - } - - var handler *namedHandler - score := 0.0 - for _, h := range healthy { - s := getNodeScore(h, ip) - if s > score { - handler = h - score = s - } - } - - log.Debug().Msgf("Service selected by HRW: %s", handler.name) - - return handler, nil -} - func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { // give ip fetched to b.nextServer clientIP := b.strategy.GetIP(req) @@ -199,3 +169,34 @@ func (b *Balancer) Add(name string, handler http.Handler, weight *int, fenced bo } b.handlersMu.Unlock() } + +func (b *Balancer) nextServer(ip string) (*namedHandler, error) { + b.handlersMu.RLock() + var healthy []*namedHandler + for _, h := range b.handlers { + if _, ok := b.status[h.name]; ok { + if _, fenced := b.fenced[h.name]; !fenced { + healthy = append(healthy, h) + } + } + } + b.handlersMu.RUnlock() + + if len(healthy) == 0 { + return nil, errNoAvailableServer + } + + var handler *namedHandler + score := 0.0 + for _, h := range healthy { + s := getNodeScore(h, ip) + if s > score { + handler = h + score = s + } + } + + log.Debug().Msgf("Service selected by HRW: %s", handler.name) + + return handler, nil +} diff --git a/pkg/server/service/loadbalancer/hrw/hrw_test.go b/pkg/server/service/loadbalancer/hrw/hrw_test.go index a843fdb3d..68e07c24b 100644 --- a/pkg/server/service/loadbalancer/hrw/hrw_test.go +++ b/pkg/server/service/loadbalancer/hrw/hrw_test.go @@ -296,6 +296,7 @@ func Int(v int) *int { return &v } type responseRecorder struct { *httptest.ResponseRecorder + save map[string]int sequence []string status []int diff --git a/pkg/server/service/loadbalancer/leasttime/leasttime.go b/pkg/server/service/loadbalancer/leasttime/leasttime.go index 41f4f5218..fcff7f2cf 100644 --- a/pkg/server/service/loadbalancer/leasttime/leasttime.go +++ b/pkg/server/service/loadbalancer/leasttime/leasttime.go @@ -23,6 +23,7 @@ var errNoAvailableServer = errors.New("no available server") // Tracks response time (TTFB) and inflight request count for load balancing decisions. type namedHandler struct { http.Handler + name string weight float64 @@ -180,98 +181,6 @@ func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error { return nil } -// getHealthyServers returns the list of healthy, non-fenced servers. -func (b *Balancer) getHealthyServers() []*namedHandler { - b.handlersMu.RLock() - defer b.handlersMu.RUnlock() - - var healthy []*namedHandler - for _, h := range b.handlers { - if _, ok := b.status[h.name]; ok { - if _, fenced := b.fenced[h.name]; !fenced { - healthy = append(healthy, h) - } - } - } - return healthy -} - -// selectWRR selects a server from candidates using Weighted Round Robin (EDF scheduling). -// This is used for tie-breaking when multiple servers have identical scores. -func (b *Balancer) selectWRR(candidates []*namedHandler) *namedHandler { - if len(candidates) == 0 { - return nil - } - - selected := candidates[0] - minDeadline := math.MaxFloat64 - - // Find handler with earliest deadline. - for _, h := range candidates { - handlerDeadline := h.getDeadline() - if handlerDeadline < minDeadline { - minDeadline = handlerDeadline - selected = h - } - } - - // Update deadline based on when this server was selected (minDeadline), - // not the global curDeadline. This ensures proper weighted distribution. - newDeadline := minDeadline + 1/selected.weight - selected.setDeadline(newDeadline) - - // Track the maximum deadline assigned for initializing new servers. - b.curDeadlineMu.Lock() - if newDeadline > b.curDeadline { - b.curDeadline = newDeadline - } - b.curDeadlineMu.Unlock() - - return selected -} - -// Score = (avgResponseTime × (1 + inflightCount)) / weight. -func (b *Balancer) nextServer() (*namedHandler, error) { - healthy := b.getHealthyServers() - - if len(healthy) == 0 { - return nil, errNoAvailableServer - } - - if len(healthy) == 1 { - return healthy[0], nil - } - - // Calculate scores and find minimum. - minScore := math.MaxFloat64 - var candidates []*namedHandler - - for _, h := range healthy { - avgRT := h.getAvgResponseTime() - inflight := float64(h.inflightCount.Load()) - score := (avgRT * (1 + inflight)) / h.weight - - if score < minScore { - minScore = score - candidates = []*namedHandler{h} - } else if score == minScore { - candidates = append(candidates, h) - } - } - - if len(candidates) == 1 { - return candidates[0], nil - } - - // Multiple servers with same score: use WRR (EDF) tie-breaking. - selected := b.selectWRR(candidates) - if selected == nil { - return nil, errNoAvailableServer - } - - return selected, nil -} - func (b *Balancer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // Handle sticky sessions first. if b.sticky != nil { @@ -371,3 +280,95 @@ func (b *Balancer) Add(name string, handler http.Handler, weight *int, fenced bo b.sticky.AddHandler(name, handler) } } + +// getHealthyServers returns the list of healthy, non-fenced servers. +func (b *Balancer) getHealthyServers() []*namedHandler { + b.handlersMu.RLock() + defer b.handlersMu.RUnlock() + + var healthy []*namedHandler + for _, h := range b.handlers { + if _, ok := b.status[h.name]; ok { + if _, fenced := b.fenced[h.name]; !fenced { + healthy = append(healthy, h) + } + } + } + return healthy +} + +// selectWRR selects a server from candidates using Weighted Round Robin (EDF scheduling). +// This is used for tie-breaking when multiple servers have identical scores. +func (b *Balancer) selectWRR(candidates []*namedHandler) *namedHandler { + if len(candidates) == 0 { + return nil + } + + selected := candidates[0] + minDeadline := math.MaxFloat64 + + // Find handler with earliest deadline. + for _, h := range candidates { + handlerDeadline := h.getDeadline() + if handlerDeadline < minDeadline { + minDeadline = handlerDeadline + selected = h + } + } + + // Update deadline based on when this server was selected (minDeadline), + // not the global curDeadline. This ensures proper weighted distribution. + newDeadline := minDeadline + 1/selected.weight + selected.setDeadline(newDeadline) + + // Track the maximum deadline assigned for initializing new servers. + b.curDeadlineMu.Lock() + if newDeadline > b.curDeadline { + b.curDeadline = newDeadline + } + b.curDeadlineMu.Unlock() + + return selected +} + +// Score = (avgResponseTime × (1 + inflightCount)) / weight. +func (b *Balancer) nextServer() (*namedHandler, error) { + healthy := b.getHealthyServers() + + if len(healthy) == 0 { + return nil, errNoAvailableServer + } + + if len(healthy) == 1 { + return healthy[0], nil + } + + // Calculate scores and find minimum. + minScore := math.MaxFloat64 + var candidates []*namedHandler + + for _, h := range healthy { + avgRT := h.getAvgResponseTime() + inflight := float64(h.inflightCount.Load()) + score := (avgRT * (1 + inflight)) / h.weight + + if score < minScore { + minScore = score + candidates = []*namedHandler{h} + } else if score == minScore { + candidates = append(candidates, h) + } + } + + if len(candidates) == 1 { + return candidates[0], nil + } + + // Multiple servers with same score: use WRR (EDF) tie-breaking. + selected := b.selectWRR(candidates) + if selected == nil { + return nil, errNoAvailableServer + } + + return selected, nil +} diff --git a/pkg/server/service/loadbalancer/leasttime/leasttime_test.go b/pkg/server/service/loadbalancer/leasttime/leasttime_test.go index 53bb81505..d444b314f 100644 --- a/pkg/server/service/loadbalancer/leasttime/leasttime_test.go +++ b/pkg/server/service/loadbalancer/leasttime/leasttime_test.go @@ -23,6 +23,7 @@ func pointer[T any](v T) *T { return &v } // responseRecorder tracks which servers handled requests. type responseRecorder struct { *httptest.ResponseRecorder + save map[string]int } diff --git a/pkg/server/service/loadbalancer/mirror/mirror.go b/pkg/server/service/loadbalancer/mirror/mirror.go index 91cf364a3..9b4dbb804 100644 --- a/pkg/server/service/loadbalancer/mirror/mirror.go +++ b/pkg/server/service/loadbalancer/mirror/mirror.go @@ -45,38 +45,15 @@ func New(handler http.Handler, pool *safe.Pool, mirrorBody bool, maxBodySize int } } -func (m *Mirroring) inc() uint64 { - m.lock.Lock() - defer m.lock.Unlock() - m.total++ - return m.total -} - type mirrorHandler struct { http.Handler + percent int lock sync.RWMutex count uint64 } -func (m *Mirroring) getActiveMirrors() []http.Handler { - total := m.inc() - - var mirrors []http.Handler - for _, handler := range m.mirrorHandlers { - handler.lock.Lock() - if handler.count*100 < total*uint64(handler.percent) { - handler.count++ - handler.lock.Unlock() - mirrors = append(mirrors, handler) - } else { - handler.lock.Unlock() - } - } - return mirrors -} - func (m *Mirroring) ServeHTTP(rw http.ResponseWriter, req *http.Request) { mirrors := m.getActiveMirrors() if len(mirrors) == 0 { @@ -165,6 +142,30 @@ func (m *Mirroring) RegisterStatusUpdater(fn func(up bool)) error { return nil } +func (m *Mirroring) inc() uint64 { + m.lock.Lock() + defer m.lock.Unlock() + m.total++ + return m.total +} + +func (m *Mirroring) getActiveMirrors() []http.Handler { + total := m.inc() + + var mirrors []http.Handler + for _, handler := range m.mirrorHandlers { + handler.lock.Lock() + if handler.count*100 < total*uint64(handler.percent) { + handler.count++ + handler.lock.Unlock() + mirrors = append(mirrors, handler) + } else { + handler.lock.Unlock() + } + } + return mirrors +} + type blackHoleResponseWriter struct{} func (b blackHoleResponseWriter) Flush() {} diff --git a/pkg/server/service/loadbalancer/p2c/p2c.go b/pkg/server/service/loadbalancer/p2c/p2c.go index 948a7d5a1..5b51cfacb 100644 --- a/pkg/server/service/loadbalancer/p2c/p2c.go +++ b/pkg/server/service/loadbalancer/p2c/p2c.go @@ -134,50 +134,6 @@ func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error { return nil } -func (b *Balancer) nextServer() (*namedHandler, error) { - // We kept the same representation (map) as in the WRR strategy to improve maintainability. - // However, with the P2C strategy, we only need a slice of healthy servers. - b.handlersMu.RLock() - var healthy []*namedHandler - for _, h := range b.handlers { - if _, ok := b.status[h.name]; ok { - if _, fenced := b.fenced[h.name]; !fenced { - healthy = append(healthy, h) - } - } - } - b.handlersMu.RUnlock() - - if len(healthy) == 0 { - return nil, errNoAvailableServer - } - - // If there is only one healthy server, return it. - if len(healthy) == 1 { - return healthy[0], nil - } - // In order to not get the same backend twice, we make the second call to s.rand.Intn one fewer - // than the length of the slice. We then have to shift over the second index if it is equal or - // greater than the first index, wrapping round if needed. - b.randMu.Lock() - n1, n2 := b.rand.Intn(len(healthy)), b.rand.Intn(len(healthy)) - b.randMu.Unlock() - - if n2 == n1 { - n2 = (n2 + 1) % len(healthy) - } - - h1, h2 := healthy[n1], healthy[n2] - // Ensure h1 has fewer inflight requests than h2. - if h2.inflight.Load() < h1.inflight.Load() { - log.Debug().Msgf("Service selected by P2C: %s", h2.name) - return h2, nil - } - - log.Debug().Msgf("Service selected by P2C: %s", h1.name) - return h1, nil -} - func (b *Balancer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if b.sticky != nil { h, rewrite, err := b.sticky.StickyHandler(req) @@ -235,3 +191,47 @@ func (b *Balancer) AddServer(name string, handler http.Handler, server dynamic.S b.sticky.AddHandler(name, h) } } + +func (b *Balancer) nextServer() (*namedHandler, error) { + // We kept the same representation (map) as in the WRR strategy to improve maintainability. + // However, with the P2C strategy, we only need a slice of healthy servers. + b.handlersMu.RLock() + var healthy []*namedHandler + for _, h := range b.handlers { + if _, ok := b.status[h.name]; ok { + if _, fenced := b.fenced[h.name]; !fenced { + healthy = append(healthy, h) + } + } + } + b.handlersMu.RUnlock() + + if len(healthy) == 0 { + return nil, errNoAvailableServer + } + + // If there is only one healthy server, return it. + if len(healthy) == 1 { + return healthy[0], nil + } + // In order to not get the same backend twice, we make the second call to s.rand.Intn one fewer + // than the length of the slice. We then have to shift over the second index if it is equal or + // greater than the first index, wrapping round if needed. + b.randMu.Lock() + n1, n2 := b.rand.Intn(len(healthy)), b.rand.Intn(len(healthy)) + b.randMu.Unlock() + + if n2 == n1 { + n2 = (n2 + 1) % len(healthy) + } + + h1, h2 := healthy[n1], healthy[n2] + // Ensure h1 has fewer inflight requests than h2. + if h2.inflight.Load() < h1.inflight.Load() { + log.Debug().Msgf("Service selected by P2C: %s", h2.name) + return h2, nil + } + + log.Debug().Msgf("Service selected by P2C: %s", h1.name) + return h1, nil +} diff --git a/pkg/server/service/loadbalancer/p2c/p2c_test.go b/pkg/server/service/loadbalancer/p2c/p2c_test.go index bce6cc456..83fe559cd 100644 --- a/pkg/server/service/loadbalancer/p2c/p2c_test.go +++ b/pkg/server/service/loadbalancer/p2c/p2c_test.go @@ -246,6 +246,7 @@ func TestBalancerAllServersFenced(t *testing.T) { type responseRecorder struct { *httptest.ResponseRecorder + save map[string]int sequence []string status []int diff --git a/pkg/server/service/loadbalancer/wrr/wrr.go b/pkg/server/service/loadbalancer/wrr/wrr.go index 69bc2c498..719db519e 100644 --- a/pkg/server/service/loadbalancer/wrr/wrr.go +++ b/pkg/server/service/loadbalancer/wrr/wrr.go @@ -16,6 +16,7 @@ var errNoAvailableServer = errors.New("no available server") type namedHandler struct { http.Handler + name string weight float64 deadline float64 @@ -78,7 +79,7 @@ func (b *Balancer) Swap(i, j int) { } // Push implements heap.Interface for pushing an item into the heap. -func (b *Balancer) Push(x interface{}) { +func (b *Balancer) Push(x any) { h, ok := x.(*namedHandler) if !ok { return @@ -89,7 +90,7 @@ func (b *Balancer) Push(x interface{}) { // Pop implements heap.Interface for popping an item from the heap. // It panics if b.Len() < 1. -func (b *Balancer) Pop() interface{} { +func (b *Balancer) Pop() any { h := b.handlers[len(b.handlers)-1] b.handlers = b.handlers[0 : len(b.handlers)-1] return h @@ -147,36 +148,6 @@ func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error { return nil } -func (b *Balancer) nextServer() (*namedHandler, error) { - b.handlersMu.Lock() - defer b.handlersMu.Unlock() - - if len(b.handlers) == 0 || len(b.status) == 0 || len(b.fenced) == len(b.handlers) { - return nil, errNoAvailableServer - } - - var handler *namedHandler - for { - // Pick handler with closest deadline. - handler = heap.Pop(b).(*namedHandler) - - // curDeadline should be handler's deadline so that new added entry would have a fair competition environment with the old ones. - b.curDeadline = handler.deadline - handler.deadline += 1 / handler.weight - - heap.Push(b, handler) - if _, ok := b.status[handler.name]; ok { - if _, ok := b.fenced[handler.name]; !ok { - // do not select a fenced handler. - break - } - } - } - - log.Debug().Msgf("Service selected by WRR: %s", handler.name) - return handler, nil -} - func (b *Balancer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if b.sticky != nil { h, rewrite, err := b.sticky.StickyHandler(req) @@ -250,3 +221,33 @@ func (b *Balancer) Add(name string, handler http.Handler, weight *int, fenced bo b.sticky.AddHandler(name, handler) } } + +func (b *Balancer) nextServer() (*namedHandler, error) { + b.handlersMu.Lock() + defer b.handlersMu.Unlock() + + if len(b.handlers) == 0 || len(b.status) == 0 || len(b.fenced) == len(b.handlers) { + return nil, errNoAvailableServer + } + + var handler *namedHandler + for { + // Pick handler with closest deadline. + handler = heap.Pop(b).(*namedHandler) + + // curDeadline should be handler's deadline so that new added entry would have a fair competition environment with the old ones. + b.curDeadline = handler.deadline + handler.deadline += 1 / handler.weight + + heap.Push(b, handler) + if _, ok := b.status[handler.name]; ok { + if _, ok := b.fenced[handler.name]; !ok { + // do not select a fenced handler. + break + } + } + } + + log.Debug().Msgf("Service selected by WRR: %s", handler.name) + return handler, nil +} diff --git a/pkg/server/service/loadbalancer/wrr/wrr_test.go b/pkg/server/service/loadbalancer/wrr/wrr_test.go index 5260623be..7c936d47b 100644 --- a/pkg/server/service/loadbalancer/wrr/wrr_test.go +++ b/pkg/server/service/loadbalancer/wrr/wrr_test.go @@ -378,6 +378,7 @@ func TestBalancerBias(t *testing.T) { type responseRecorder struct { *httptest.ResponseRecorder + save map[string]int sequence []string status []int diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index e61820f3d..40b1ae474 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -171,6 +171,14 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H return lb, nil } +// LaunchHealthCheck launches the health checks. +func (m *Manager) LaunchHealthCheck(ctx context.Context) { + for serviceName, hc := range m.healthCheckers { + logger := log.Ctx(ctx).With().Str(logs.ServiceName, serviceName).Logger() + go hc.Launch(logger.WithContext(ctx)) + } +} + func (m *Manager) getFailoverServiceHandler(ctx context.Context, serviceName string, config *dynamic.Failover) (http.Handler, error) { f := failover.New(config.HealthCheck) @@ -360,13 +368,6 @@ func (m *Manager) getHRWServiceHandler(ctx context.Context, serviceName string, return balancer, nil } -type serverBalancer interface { - http.Handler - healthcheck.StatusSetter - - AddServer(name string, handler http.Handler, server dynamic.Server) -} - func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName string, info *runtime.ServiceInfo) (http.Handler, error) { service := info.LoadBalancer @@ -494,12 +495,11 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName return lb, nil } -// LaunchHealthCheck launches the health checks. -func (m *Manager) LaunchHealthCheck(ctx context.Context) { - for serviceName, hc := range m.healthCheckers { - logger := log.Ctx(ctx).With().Str(logs.ServiceName, serviceName).Logger() - go hc.Launch(logger.WithContext(ctx)) - } +type serverBalancer interface { + http.Handler + healthcheck.StatusSetter + + AddServer(name string, handler http.Handler, server dynamic.Server) } func shuffle[T any](values []T, r *rand.Rand) []T { diff --git a/pkg/tcp/dialer.go b/pkg/tcp/dialer.go index da9a24569..82243d74e 100644 --- a/pkg/tcp/dialer.go +++ b/pkg/tcp/dialer.go @@ -74,6 +74,7 @@ func (d tcpDialer) DialContext(ctx context.Context, network, addr string, client type tcpTLSDialer struct { tcpDialer + tlsConfig *tls.Config } diff --git a/pkg/tcp/proxy_unix.go b/pkg/tcp/proxy_unix.go index 727074bcc..b78361416 100644 --- a/pkg/tcp/proxy_unix.go +++ b/pkg/tcp/proxy_unix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package tcp diff --git a/pkg/tcp/proxy_windows.go b/pkg/tcp/proxy_windows.go index 579d52063..8d9480135 100644 --- a/pkg/tcp/proxy_windows.go +++ b/pkg/tcp/proxy_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows package tcp diff --git a/pkg/tcp/wrr_load_balancer.go b/pkg/tcp/wrr_load_balancer.go index c836af17d..4c080599d 100644 --- a/pkg/tcp/wrr_load_balancer.go +++ b/pkg/tcp/wrr_load_balancer.go @@ -12,6 +12,7 @@ var errNoServersInPool = errors.New("no servers in the pool") type server struct { Handler + name string weight int } diff --git a/pkg/testhelpers/metrics.go b/pkg/testhelpers/metrics.go index bc41c2c3f..2267ce98c 100644 --- a/pkg/testhelpers/metrics.go +++ b/pkg/testhelpers/metrics.go @@ -46,12 +46,12 @@ type CollectingHealthCheckMetrics struct { Gauge *CollectingGauge } -// BackendServerUpGauge is there to satisfy the healthcheck.metricsRegistry interface. -func (m *CollectingHealthCheckMetrics) BackendServerUpGauge() metrics.Gauge { - return m.Gauge -} - // NewCollectingHealthCheckMetrics creates a new CollectingHealthCheckMetrics instance. func NewCollectingHealthCheckMetrics() *CollectingHealthCheckMetrics { return &CollectingHealthCheckMetrics{&CollectingGauge{}} } + +// BackendServerUpGauge is there to satisfy the healthcheck.metricsRegistry interface. +func (m *CollectingHealthCheckMetrics) BackendServerUpGauge() metrics.Gauge { + return m.Gauge +} diff --git a/pkg/tls/certificate_store.go b/pkg/tls/certificate_store.go index 57979f45d..5321f98aa 100644 --- a/pkg/tls/certificate_store.go +++ b/pkg/tls/certificate_store.go @@ -102,7 +102,7 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) matchedCerts := map[string]*CertificateData{} if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil { for domains, cert := range c.DynamicCerts.Get().(map[string]*CertificateData) { - for _, certDomain := range strings.Split(domains, ",") { + for certDomain := range strings.SplitSeq(domains, ",") { if matchDomain(serverName, certDomain) { matchedCerts[certDomain] = cert } @@ -157,7 +157,7 @@ func (c *CertificateStore) GetCertificate(domains []string) *CertificateData { } var matchedDomains []string - for _, certDomain := range strings.Split(certDomains, ",") { + for certDomain := range strings.SplitSeq(certDomains, ",") { for _, checkDomain := range domains { if certDomain == checkDomain { matchedDomains = append(matchedDomains, certDomain) diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index d98fc9483..e3119e833 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -61,5 +61,6 @@ type GeneratedCert struct { // CertAndStores allows mapping a TLS certificate to a list of entry points. type CertAndStores struct { Certificate `yaml:",inline" export:"true"` - Stores []string `json:"stores,omitempty" toml:"stores,omitempty" yaml:"stores,omitempty" export:"true"` + + Stores []string `json:"stores,omitempty" toml:"stores,omitempty" yaml:"stores,omitempty" export:"true"` } diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index 54d2c4cbf..edd5987d9 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -333,6 +333,14 @@ func (m *Manager) GetServerCertificates() []*x509.Certificate { return certificates } +// GetStore gets the certificate store of a given name. +func (m *Manager) GetStore(storeName string) *CertificateStore { + m.lock.RLock() + defer m.lock.RUnlock() + + return m.getStore(storeName) +} + // getStore returns the store found for storeName, or nil otherwise. func (m *Manager) getStore(storeName string) *CertificateStore { st, ok := m.stores[storeName] @@ -342,14 +350,6 @@ func (m *Manager) getStore(storeName string) *CertificateStore { return st } -// GetStore gets the certificate store of a given name. -func (m *Manager) GetStore(storeName string) *CertificateStore { - m.lock.RLock() - defer m.lock.RUnlock() - - return m.getStore(storeName) -} - func (m *Manager) getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateStore) (*CertificateData, error) { if tlsStore.DefaultCertificate != nil { cert, err := m.buildDefaultCertificate(tlsStore.DefaultCertificate) diff --git a/pkg/udp/conn.go b/pkg/udp/conn.go index 36778f648..5e50af39c 100644 --- a/pkg/udp/conn.go +++ b/pkg/udp/conn.go @@ -98,19 +98,6 @@ func (l *Listener) Close() error { return l.Shutdown(0) } -// close should not be called more than once. -func (l *Listener) close() error { - l.mu.Lock() - defer l.mu.Unlock() - err := l.pConn.Close() - for k, v := range l.conns { - v.close() - delete(l.conns, k) - } - close(l.acceptCh) - return err -} - // Shutdown closes the listener. // It immediately stops accepting new sessions, // and it waits for all existing sessions to terminate, @@ -125,10 +112,7 @@ func (l *Listener) Shutdown(graceTimeout time.Duration) error { l.accepting = false l.mu.Unlock() - retryInterval := closeRetryInterval - if retryInterval > graceTimeout { - retryInterval = graceTimeout - } + retryInterval := min(closeRetryInterval, graceTimeout) start := time.Now() end := start.Add(graceTimeout) for !time.Now().After(end) { @@ -144,6 +128,19 @@ func (l *Listener) Shutdown(graceTimeout time.Duration) error { return l.close() } +// close should not be called more than once. +func (l *Listener) close() error { + l.mu.Lock() + defer l.mu.Unlock() + err := l.pConn.Close() + for k, v := range l.conns { + v.close() + delete(l.conns, k) + } + close(l.acceptCh) + return err +} + // readLoop receives all packets from all remotes. // If a packet comes from a remote that is already known to us (i.e. a "session"), // we find that session, and otherwise we create a new one. @@ -224,6 +221,44 @@ type Conn struct { doneCh chan struct{} } +// Read reads up to len(p) bytes into p from the connection. +// Each call corresponds to at most one datagram. +// If p is smaller than the datagram, the extra bytes will be discarded. +func (c *Conn) Read(p []byte) (int, error) { + select { + case c.readCh <- p: + n := <-c.sizeCh + c.muActivity.Lock() + c.lastActivity = time.Now() + c.muActivity.Unlock() + return n, nil + + case <-c.doneCh: + return 0, io.EOF + } +} + +// Write writes len(p) bytes from p to the underlying connection. +// Each call sends at most one datagram. +// It is an error to send a message larger than the system's max UDP datagram size. +func (c *Conn) Write(p []byte) (n int, err error) { + c.muActivity.Lock() + c.lastActivity = time.Now() + c.muActivity.Unlock() + + return c.listener.pConn.WriteTo(p, c.rAddr) +} + +// Close releases resources related to the Conn. +func (c *Conn) Close() error { + c.close() + + c.listener.mu.Lock() + defer c.listener.mu.Unlock() + delete(c.listener.conns, c.rAddr.String()) + return nil +} + // readLoop waits for data to come from the listener's readLoop. // It then waits for a Read operation to be ready to consume said data, // that is to say it waits on readCh to receive the slice of bytes that the Read operation wants to read onto. @@ -269,46 +304,8 @@ func (c *Conn) readLoop() { } } -// Read reads up to len(p) bytes into p from the connection. -// Each call corresponds to at most one datagram. -// If p is smaller than the datagram, the extra bytes will be discarded. -func (c *Conn) Read(p []byte) (int, error) { - select { - case c.readCh <- p: - n := <-c.sizeCh - c.muActivity.Lock() - c.lastActivity = time.Now() - c.muActivity.Unlock() - return n, nil - - case <-c.doneCh: - return 0, io.EOF - } -} - -// Write writes len(p) bytes from p to the underlying connection. -// Each call sends at most one datagram. -// It is an error to send a message larger than the system's max UDP datagram size. -func (c *Conn) Write(p []byte) (n int, err error) { - c.muActivity.Lock() - c.lastActivity = time.Now() - c.muActivity.Unlock() - - return c.listener.pConn.WriteTo(p, c.rAddr) -} - func (c *Conn) close() { c.doneOnce.Do(func() { close(c.doneCh) }) } - -// Close releases resources related to the Conn. -func (c *Conn) Close() error { - c.close() - - c.listener.mu.Lock() - defer c.listener.mu.Unlock() - delete(c.listener.conns, c.rAddr.String()) - return nil -} diff --git a/pkg/udp/wrr_load_balancer.go b/pkg/udp/wrr_load_balancer.go index 19c1b0218..753acc8ce 100644 --- a/pkg/udp/wrr_load_balancer.go +++ b/pkg/udp/wrr_load_balancer.go @@ -9,6 +9,7 @@ import ( type server struct { Handler + weight int }