From ee3e7cbbecc1efbe2462a7c31f3417da5ea3da22 Mon Sep 17 00:00:00 2001 From: Marvin Stenger Date: Thu, 25 Apr 2024 14:54:04 +0200 Subject: [PATCH 01/26] chore: patch migration/v2.md --- docs/content/migration/v2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 16b94e577..979fc75e7 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -510,7 +510,7 @@ In `v2.10`, the Kubernetes CRDs API Group `traefik.containo.us` is deprecated, a As the Kubernetes CRD provider still works with both API Versions (`traefik.io/v1alpha1` and `traefik.containo.us/v1alpha1`), it means that for the same kind, namespace and name, the provider will only keep the `traefik.io/v1alpha1` resource. -In addition, the Kubernetes CRDs API Version `traefik.io/v1alpha1` will not be supported in Traefik v3 itself. +In addition, the Kubernetes CRDs API Version `traefik.containo.us/v1alpha1` will not be supported in Traefik v3 itself. Please note that it is a requirement to update the CRDs and the RBAC in the cluster before upgrading Traefik. To do so, please apply the required [CRDs](https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml) and [RBAC](https://raw.githubusercontent.com/traefik/traefik/v2.10/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml) manifests for v2.10: From 73e5dbbfe5febb8ee51b87b864026397248dffd9 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:44:03 +0200 Subject: [PATCH 02/26] Update Kubernetes version for v3 Helm chart --- docs/content/getting-started/install-traefik.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/getting-started/install-traefik.md b/docs/content/getting-started/install-traefik.md index 630202288..57d15dd63 100644 --- a/docs/content/getting-started/install-traefik.md +++ b/docs/content/getting-started/install-traefik.md @@ -44,7 +44,7 @@ Traefik can be installed in Kubernetes using the Helm chart from Date: Mon, 29 Apr 2024 15:50:04 +0530 Subject: [PATCH 03/26] Native Kubernetes service load-balancing at the provider level --- docs/content/providers/kubernetes-crd.md | 24 ++ docs/content/providers/kubernetes-ingress.md | 24 ++ .../reference/static-configuration/cli-ref.md | 6 + .../reference/static-configuration/env-ref.md | 6 + .../reference/static-configuration/file.toml | 2 + .../reference/static-configuration/file.yaml | 2 + .../tcp/with_global_native_service_lb.yml | 15 + .../udp/with_global_native_service_lb.yml | 14 + .../with_global_native_service_lb.yml | 16 + pkg/provider/kubernetes/crd/kubernetes.go | 1 + .../kubernetes/crd/kubernetes_http.go | 34 +- pkg/provider/kubernetes/crd/kubernetes_tcp.go | 22 +- .../kubernetes/crd/kubernetes_test.go | 336 ++++++++++++++++++ pkg/provider/kubernetes/crd/kubernetes_udp.go | 22 +- .../crd/traefikio/v1alpha1/ingressroute.go | 2 +- .../crd/traefikio/v1alpha1/ingressroutetcp.go | 2 +- .../crd/traefikio/v1alpha1/ingressrouteudp.go | 2 +- .../v1alpha1/zz_generated.deepcopy.go | 15 + .../kubernetes/ingress/annotations.go | 2 +- .../kubernetes/ingress/annotations_test.go | 2 +- .../Ingress-with-native-lb-by-default.yml | 30 ++ pkg/provider/kubernetes/ingress/kubernetes.go | 32 +- .../kubernetes/ingress/kubernetes_test.go | 81 +++++ 23 files changed, 642 insertions(+), 50 deletions(-) create mode 100644 pkg/provider/kubernetes/crd/fixtures/tcp/with_global_native_service_lb.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/udp/with_global_native_service_lb.yml create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_global_native_service_lb.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-with-native-lb-by-default.yml diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index f0ef2da39..2f60a5868 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -337,6 +337,30 @@ providers: --providers.kubernetescrd.allowexternalnameservices=true ``` +### `nativeLBByDefault` + +_Optional, Default: false_ + +Defines whether to use Native Kubernetes load-balancing mode by default. +For more information, please check out the IngressRoute `nativeLB` option [documentation](../routing/providers/kubernetes-crd.md#load-balancing). + +```yaml tab="File (YAML)" +providers: + kubernetesCRD: + nativeLBByDefault: true + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesCRD] + nativeLBByDefault = true + # ... +``` + +```bash tab="CLI" +--providers.kubernetescrd.nativeLBByDefault=true +``` + ## Full Example For additional information, refer to the [full example](../user-guides/crd-acme/index.md) with Let's Encrypt. diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index dc08cde09..f60ed243a 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -467,6 +467,30 @@ providers: --providers.kubernetesingress.allowexternalnameservices=true ``` +### `nativeLBByDefault` + +_Optional, Default: false_ + +Defines whether to use Native Kubernetes load-balancing mode by default. +For more information, please check out the `traefik.ingress.kubernetes.io/service.nativelb` [service annotation documentation](../routing/providers/kubernetes-ingress.md#on-service). + +```yaml tab="File (YAML)" +providers: + kubernetesIngress: + nativeLBByDefault: true + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesIngress] + nativeLBByDefault = true + # ... +``` + +```bash tab="CLI" +--providers.kubernetesingress.nativeLBByDefault=true +``` + ### Further To learn more about the various aspects of the Ingress specification that Traefik supports, diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index d0d339bf1..99065ee63 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -714,6 +714,9 @@ Kubernetes label selector to use. `--providers.kubernetescrd.namespaces`: Kubernetes namespaces. +`--providers.kubernetescrd.nativelbbydefault`: +Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```) + `--providers.kubernetescrd.throttleduration`: Ingress refresh throttle duration (Default: ```0```) @@ -795,6 +798,9 @@ Kubernetes Ingress label selector to use. `--providers.kubernetesingress.namespaces`: Kubernetes namespaces. +`--providers.kubernetesingress.nativelbbydefault`: +Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```) + `--providers.kubernetesingress.throttleduration`: Ingress refresh throttle duration (Default: ```0```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 5c4242bd5..5d8313abb 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -714,6 +714,9 @@ Kubernetes label selector to use. `TRAEFIK_PROVIDERS_KUBERNETESCRD_NAMESPACES`: Kubernetes namespaces. +`TRAEFIK_PROVIDERS_KUBERNETESCRD_NATIVELBBYDEFAULT`: +Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```) + `TRAEFIK_PROVIDERS_KUBERNETESCRD_THROTTLEDURATION`: Ingress refresh throttle duration (Default: ```0```) @@ -795,6 +798,9 @@ Kubernetes Ingress label selector to use. `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_NAMESPACES`: Kubernetes namespaces. +`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_NATIVELBBYDEFAULT`: +Defines whether to use Native Kubernetes load-balancing mode by default. (Default: ```false```) + `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_THROTTLEDURATION`: Ingress refresh throttle duration (Default: ```0```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 9d796c4a6..c3ee80b19 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -124,6 +124,7 @@ allowEmptyServices = true allowExternalNameServices = true disableIngressClassLookup = true + nativeLBByDefault = true [providers.kubernetesIngress.ingressEndpoint] ip = "foobar" hostname = "foobar" @@ -139,6 +140,7 @@ ingressClass = "foobar" throttleDuration = "42s" allowEmptyServices = true + nativeLBByDefault = true [providers.kubernetesGateway] endpoint = "foobar" token = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 477fa6b0e..f96e50f99 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -141,6 +141,7 @@ providers: allowEmptyServices: true allowExternalNameServices: true disableIngressClassLookup: true + nativeLBByDefault: true kubernetesCRD: endpoint: foobar token: foobar @@ -154,6 +155,7 @@ providers: ingressClass: foobar throttleDuration: 42s allowEmptyServices: true + nativeLBByDefault: true kubernetesGateway: endpoint: foobar token: foobar diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_global_native_service_lb.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_global_native_service_lb.yml new file mode 100644 index 000000000..d8832498e --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_global_native_service_lb.yml @@ -0,0 +1,15 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRouteTCP +metadata: + name: global-native-lb + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: native-svc-tcp + port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_global_native_service_lb.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_global_native_service_lb.yml new file mode 100644 index 000000000..5e7dbd2e5 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_global_native_service_lb.yml @@ -0,0 +1,14 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRouteUDP +metadata: + name: global-native-lb + namespace: default + +spec: + entryPoints: + - foo + + routes: + - services: + - name: native-svc-udp + port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_global_native_service_lb.yml b/pkg/provider/kubernetes/crd/fixtures/with_global_native_service_lb.yml new file mode 100644 index 000000000..9b8fa2581 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_global_native_service_lb.yml @@ -0,0 +1,16 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: global-native-lb + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) + kind: Rule + services: + - name: native-svc + port: 80 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 1559e44e9..298bf884f 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -60,6 +60,7 @@ type Provider struct { IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` AllowEmptyServices bool `description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` + NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing mode by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"` lastConfiguration safe.Safe diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 6e3b1f2a5..e7364482c 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -55,6 +55,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices, allowEmptyServices: p.AllowEmptyServices, + NativeLBByDefault: p.NativeLBByDefault, } for _, route := range ingressRoute.Spec.Routes { @@ -202,6 +203,7 @@ type configBuilder struct { allowCrossNamespace bool allowExternalNameServices bool allowEmptyServices bool + NativeLBByDefault bool } // buildTraefikService creates the configuration for the traefik service defined in tService, @@ -377,20 +379,6 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L return nil, err } - if svc.NativeLB { - address, err := getNativeServiceAddress(*service, *svcPort) - if err != nil { - return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err) - } - - protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port) - if err != nil { - return nil, err - } - - return []dynamic.Server{{URL: fmt.Sprintf("%s://%s", protocol, address)}}, nil - } - var servers []dynamic.Server if service.Spec.Type == corev1.ServiceTypeExternalName { if !c.allowExternalNameServices { @@ -409,6 +397,24 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L }), nil } + nativeLB := c.NativeLBByDefault + if svc.NativeLB != nil { + nativeLB = *svc.NativeLB + } + if nativeLB { + address, err := getNativeServiceAddress(*service, *svcPort) + if err != nil { + return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err) + } + + protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port) + if err != nil { + return nil, err + } + + return []dynamic.Server{{URL: fmt.Sprintf("%s://%s", protocol, address)}}, nil + } + endpoints, endpointsExists, endpointsErr := c.client.GetEndpoints(namespace, sanitizedName) if endpointsErr != nil { return nil, endpointsErr diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index f41fe125e..ea955adae 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -237,21 +237,25 @@ func (p *Provider) loadTCPServers(client Client, namespace string, svc traefikv1 return nil, err } - if svc.NativeLB { - address, err := getNativeServiceAddress(*service, *svcPort) - if err != nil { - return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err) - } - - return []dynamic.TCPServer{{Address: address}}, nil - } - var servers []dynamic.TCPServer if service.Spec.Type == corev1.ServiceTypeExternalName { servers = append(servers, dynamic.TCPServer{ Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))), }) } else { + nativeLB := p.NativeLBByDefault + if svc.NativeLB != nil { + nativeLB = *svc.NativeLB + } + if nativeLB { + address, err := getNativeServiceAddress(*service, *svcPort) + if err != nil { + return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err) + } + + return []dynamic.TCPServer{{Address: address}}, nil + } + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) if endpointsErr != nil { return nil, endpointsErr diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index cbb36a55c..ca6b1772f 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -7180,3 +7180,339 @@ func (p *extensionBuilderRegistryMock) RegisterBackendFuncs(group, kind string, p.groupKindBackendFuncs[group][kind] = builderFunc } + +func TestGlobalNativeLB(t *testing.T) { + testCases := []struct { + desc string + ingressClass string + paths []string + NativeLBByDefault bool + expected *dynamic.Configuration + }{ + { + desc: "Empty", + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "HTTP with global native Service LB", + paths: []string{"services.yml", "with_global_native_service_lb.yml"}, + NativeLBByDefault: true, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{ + "default-global-native-lb-6f97418635c7e18853da": { + EntryPoints: []string{"foo"}, + Service: "default-global-native-lb-6f97418635c7e18853da", + Rule: "Host(`foo.com`)", + Priority: 0, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-global-native-lb-6f97418635c7e18853da": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval}, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "HTTP with native Service LB in ingressroute", + paths: []string{"services.yml", "with_native_service_lb.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{ + "default-test-route-6f97418635c7e18853da": { + EntryPoints: []string{"foo"}, + Service: "default-test-route-6f97418635c7e18853da", + Rule: "Host(`foo.com`)", + Priority: 0, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test-route-6f97418635c7e18853da": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval}, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "TCP with global native Service LB", + paths: []string{"tcp/services.yml", "tcp/with_global_native_service_lb.yml"}, + NativeLBByDefault: true, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TCP: &dynamic.TCPConfiguration{ + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + Routers: map[string]*dynamic.TCPRouter{ + "default-global-native-lb-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default-global-native-lb-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "default-global-native-lb-fdd3e9338e47a45efefc": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + }, + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "TCP with native Service LB in ingressroute", + paths: []string{"tcp/services.yml", "tcp/with_native_service_lb.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TCP: &dynamic.TCPConfiguration{ + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + Routers: map[string]*dynamic.TCPRouter{ + "default-test.route-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "default-test.route-fdd3e9338e47a45efefc": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + }, + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "UDP with native Service LB in ingressroute", + paths: []string{"udp/services.yml", "udp/with_native_service_lb.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "default-test.route-0": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-0", + }, + }, + Services: map[string]*dynamic.UDPService{ + "default-test.route-0": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + }, + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TCP: &dynamic.TCPConfiguration{ + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "UDP with global native Service LB", + paths: []string{"udp/services.yml", "udp/with_global_native_service_lb.yml"}, + NativeLBByDefault: true, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "default-global-native-lb-0": { + EntryPoints: []string{"foo"}, + Service: "default-global-native-lb-0", + }, + }, + Services: map[string]*dynamic.UDPService{ + "default-global-native-lb-0": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "10.10.0.1:8000", + Port: "", + }, + }, + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TCP: &dynamic.TCPConfiguration{ + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var k8sObjects []runtime.Object + var crdObjects []runtime.Object + for _, path := range test.paths { + yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path)) + if err != nil { + panic(err) + } + + objects := k8s.MustParseYaml(yamlContent) + for _, obj := range objects { + switch o := obj.(type) { + case *corev1.Service, *corev1.Endpoints, *corev1.Secret: + k8sObjects = append(k8sObjects, o) + case *traefikv1alpha1.IngressRoute: + crdObjects = append(crdObjects, o) + case *traefikv1alpha1.IngressRouteTCP: + crdObjects = append(crdObjects, o) + case *traefikv1alpha1.IngressRouteUDP: + crdObjects = append(crdObjects, o) + case *traefikv1alpha1.Middleware: + crdObjects = append(crdObjects, o) + case *traefikv1alpha1.TraefikService: + crdObjects = append(crdObjects, o) + case *traefikv1alpha1.TLSOption: + crdObjects = append(crdObjects, o) + case *traefikv1alpha1.TLSStore: + crdObjects = append(crdObjects, o) + default: + } + } + } + + kubeClient := kubefake.NewSimpleClientset(k8sObjects...) + crdClient := traefikcrdfake.NewSimpleClientset(crdObjects...) + + client := newClientImpl(kubeClient, crdClient) + + stopCh := make(chan struct{}) + + eventCh, err := client.WatchAll([]string{"default", "cross-ns"}, stopCh) + require.NoError(t, err) + + if k8sObjects != nil || crdObjects != nil { + // just wait for the first event + <-eventCh + } + + p := Provider{NativeLBByDefault: test.NativeLBByDefault} + + conf := p.loadConfigurationFromCRD(context.Background(), client) + assert.Equal(t, test.expected, conf) + }) + } +} diff --git a/pkg/provider/kubernetes/crd/kubernetes_udp.go b/pkg/provider/kubernetes/crd/kubernetes_udp.go index 417e90d99..c98d0212a 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_udp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_udp.go @@ -121,21 +121,25 @@ func (p *Provider) loadUDPServers(client Client, namespace string, svc traefikv1 return nil, err } - if svc.NativeLB { - address, err := getNativeServiceAddress(*service, *svcPort) - if err != nil { - return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err) - } - - return []dynamic.UDPServer{{Address: address}}, nil - } - var servers []dynamic.UDPServer if service.Spec.Type == corev1.ServiceTypeExternalName { servers = append(servers, dynamic.UDPServer{ Address: net.JoinHostPort(service.Spec.ExternalName, strconv.Itoa(int(svcPort.Port))), }) } else { + nativeLB := p.NativeLBByDefault + if svc.NativeLB != nil { + nativeLB = *svc.NativeLB + } + if nativeLB { + address, err := getNativeServiceAddress(*service, *svcPort) + if err != nil { + return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err) + } + + return []dynamic.UDPServer{{Address: address}}, nil + } + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) if endpointsErr != nil { return nil, endpointsErr diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go index 475d160d4..89718b745 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go @@ -125,7 +125,7 @@ type LoadBalancerSpec struct { // whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. // The Kubernetes Service itself does load-balance to the pods. // By default, NativeLB is false. - NativeLB bool `json:"nativeLB,omitempty"` + NativeLB *bool `json:"nativeLB,omitempty"` } type ResponseForwarding struct { diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go index 9ff6897db..e37050f19 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go @@ -92,7 +92,7 @@ type ServiceTCP struct { // whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. // The Kubernetes Service itself does load-balance to the pods. // By default, NativeLB is false. - NativeLB bool `json:"nativeLB,omitempty"` + NativeLB *bool `json:"nativeLB,omitempty"` } // +genclient diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressrouteudp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressrouteudp.go index 18773f437..c77559a50 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressrouteudp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressrouteudp.go @@ -37,7 +37,7 @@ type ServiceUDP struct { // whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. // The Kubernetes Service itself does load-balance to the pods. // By default, NativeLB is false. - NativeLB bool `json:"nativeLB,omitempty"` + NativeLB *bool `json:"nativeLB,omitempty"` } // +genclient diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go index 547f0de4e..a17ff5484 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -577,6 +577,11 @@ func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) { *out = new(int) **out = **in } + if in.NativeLB != nil { + in, out := &in.NativeLB, &out.NativeLB + *out = new(bool) + **out = **in + } return } @@ -1333,6 +1338,11 @@ func (in *ServiceTCP) DeepCopyInto(out *ServiceTCP) { *out = new(dynamic.ProxyProtocol) **out = **in } + if in.NativeLB != nil { + in, out := &in.NativeLB, &out.NativeLB + *out = new(bool) + **out = **in + } return } @@ -1355,6 +1365,11 @@ func (in *ServiceUDP) DeepCopyInto(out *ServiceUDP) { *out = new(int) **out = **in } + if in.NativeLB != nil { + in, out := &in.NativeLB, &out.NativeLB + *out = new(bool) + **out = **in + } return } diff --git a/pkg/provider/kubernetes/ingress/annotations.go b/pkg/provider/kubernetes/ingress/annotations.go index 134b2eda2..d3bed8cc0 100644 --- a/pkg/provider/kubernetes/ingress/annotations.go +++ b/pkg/provider/kubernetes/ingress/annotations.go @@ -45,7 +45,7 @@ type ServiceIng struct { ServersTransport string `json:"serversTransport,omitempty"` PassHostHeader *bool `json:"passHostHeader"` Sticky *dynamic.Sticky `json:"sticky,omitempty" label:"allowEmpty"` - NativeLB bool `json:"nativeLB,omitempty"` + NativeLB *bool `json:"nativeLB,omitempty"` } // SetDefaults sets the default values. diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go index cab70357f..646261894 100644 --- a/pkg/provider/kubernetes/ingress/annotations_test.go +++ b/pkg/provider/kubernetes/ingress/annotations_test.go @@ -125,7 +125,7 @@ func Test_parseServiceConfig(t *testing.T) { ServersScheme: "protocol", ServersTransport: "foobar@file", PassHostHeader: Bool(true), - NativeLB: true, + NativeLB: Bool(true), }, }, }, diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-native-lb-by-default.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-native-lb-by-default.yml new file mode 100644 index 000000000..26d1cbf59 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-native-lb-by-default.yml @@ -0,0 +1,30 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: global-native-lb + namespace: default +spec: + rules: + - host: traefik.tchouk + http: + paths: + - path: /bar + backend: + service: + name: service1 + port: + number: 8080 + pathType: Prefix + +--- +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: default +spec: + ports: + - port: 8080 + clusterIP: 10.0.0.1 + type: ClusterIP + externalName: traefik.wtf diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 99a8351f8..f3f4aa23e 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -52,6 +52,7 @@ type Provider struct { AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"` AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"` DisableIngressClassLookup bool `description:"Disables the lookup of IngressClasses." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true"` + NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing mode by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"` lastConfiguration safe.Safe @@ -571,6 +572,8 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In return nil, err } + nativeLB := p.NativeLBByDefault + if svcConfig != nil && svcConfig.Service != nil { svc.LoadBalancer.Sticky = svcConfig.Service.Sticky @@ -582,19 +585,8 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In svc.LoadBalancer.ServersTransport = svcConfig.Service.ServersTransport } - if svcConfig.Service.NativeLB { - address, err := getNativeServiceAddress(*service, portSpec) - if err != nil { - return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err) - } - - protocol := getProtocol(portSpec, portSpec.Name, svcConfig) - - svc.LoadBalancer.Servers = []dynamic.Server{ - {URL: fmt.Sprintf("%s://%s", protocol, address)}, - } - - return svc, nil + if svcConfig.Service.NativeLB != nil { + nativeLB = *svcConfig.Service.NativeLB } } @@ -609,6 +601,20 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In return svc, nil } + if nativeLB { + address, err := getNativeServiceAddress(*service, portSpec) + if err != nil { + return nil, fmt.Errorf("getting native Kubernetes Service address: %w", err) + } + + protocol := getProtocol(portSpec, portSpec.Name, svcConfig) + svc.LoadBalancer.Servers = []dynamic.Server{ + {URL: fmt.Sprintf("%s://%s", protocol, address)}, + } + + return svc, nil + } + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, backend.Service.Name) if endpointsErr != nil { return nil, endpointsErr diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index a11fc0822..273a99260 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -1864,3 +1864,84 @@ func TestGetCertificates(t *testing.T) { }) } } + +func TestLoadConfigurationFromIngressesWithNativeLBByDefault(t *testing.T) { + testCases := []struct { + desc string + ingressClass string + expected *dynamic.Configuration + }{ + { + desc: "Ingress with native service lb", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-traefik-tchouk-bar": { + Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Service: "testing-service1-8080", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval}, + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.0.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "Ingress with native lb by default", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "default-global-native-lb-traefik-tchouk-bar": { + Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Service: "default-service1-8080", + }, + }, + Services: map[string]*dynamic.Service{ + "default-service1-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval}, + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.0.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + clientMock := newClientMock(generateTestFilename(test.desc)) + + p := Provider{ + IngressClass: test.ingressClass, + NativeLBByDefault: true, + } + conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) + + assert.Equal(t, test.expected, conf) + }) + } +} From d99d2f95e66a28ee8180634dc80fdf13ba68c62a Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 29 Apr 2024 16:06:04 +0200 Subject: [PATCH 04/26] Prepare release v3.0.0 --- CHANGELOG.md | 100 ++++++++++++++++++++ script/gcg/traefik-final-release-part1.toml | 10 +- script/gcg/traefik-final-release-part2.toml | 8 +- 3 files changed, 109 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 866bbc10b..7045623db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,103 @@ +## [v3.0.0](https://github.com/traefik/traefik/tree/v3.0.0) (2024-04-29) +[All Commits](https://github.com/traefik/traefik/compare/v3.0.0-beta1...v3.0.0) + +**Enhancements:** +- **[consul]** ConsulCatalog StrictChecks ([#10388](https://github.com/traefik/traefik/pull/10388) by [djenriquez](https://github.com/djenriquez)) +- **[ecs]** Add option to keep only healthy ECS tasks ([#8027](https://github.com/traefik/traefik/pull/8027) by [Michampt](https://github.com/Michampt)) +- **[healthcheck]** Support gRPC healthcheck ([#8583](https://github.com/traefik/traefik/pull/8583) by [jjacque](https://github.com/jjacque)) +- **[healthcheck]** Add a status option to the service health check ([#9463](https://github.com/traefik/traefik/pull/9463) by [guoard](https://github.com/guoard)) +- **[http]** Support custom headers when fetching configuration through HTTP ([#9421](https://github.com/traefik/traefik/pull/9421) by [kevinpollet](https://github.com/kevinpollet)) +- **[http3]** Moves HTTP/3 outside the experimental section ([#9570](https://github.com/traefik/traefik/pull/9570) by [sdelicata](https://github.com/sdelicata)) +- **[k8s/gatewayapi]** Add option to set Gateway status address ([#10582](https://github.com/traefik/traefik/pull/10582) by [kevinpollet](https://github.com/kevinpollet)) +- **[k8s/gatewayapi]** Toggle support for experimental channel ([#10435](https://github.com/traefik/traefik/pull/10435) by [SantoDE](https://github.com/SantoDE)) +- **[k8s/gatewayapi]** Handle middlewares in filters extension reference ([#10511](https://github.com/traefik/traefik/pull/10511) by [youkoulayley](https://github.com/youkoulayley)) +- **[k8s/ingress,k8s/crd,k8s,k8s/gatewayapi]** Use runtime.Object in routerTransform ([#10523](https://github.com/traefik/traefik/pull/10523) by [juliens](https://github.com/juliens)) +- **[k8s/crd,k8s]** Allow to use internal node IPs for NodePort services ([#10278](https://github.com/traefik/traefik/pull/10278) by [jorisvergeer](https://github.com/jorisvergeer)) +- **[logs,performance]** New logger for the Traefik logs ([#9515](https://github.com/traefik/traefik/pull/9515) by [ldez](https://github.com/ldez)) +- **[logs,plugins]** Retry on plugin API calls ([#9530](https://github.com/traefik/traefik/pull/9530) by [ldez](https://github.com/ldez)) +- **[logs,provider]** Improve provider logs ([#9562](https://github.com/traefik/traefik/pull/9562) by [ldez](https://github.com/ldez)) +- **[logs]** Improve test logger assertions ([#9533](https://github.com/traefik/traefik/pull/9533) by [ldez](https://github.com/ldez)) +- **[metrics,tracing]** Upgrade opentelemetry dependencies ([#10472](https://github.com/traefik/traefik/pull/10472) by [mmatur](https://github.com/mmatur)) +- **[metrics]** Support gRPC and gRPC-Web protocol in metrics ([#9483](https://github.com/traefik/traefik/pull/9483) by [longit644](https://github.com/longit644)) +- **[middleware,accesslogs]** Log TLS client subject ([#9285](https://github.com/traefik/traefik/pull/9285) by [xmessi](https://github.com/xmessi)) +- **[middleware,metrics,tracing,otel]** Add OpenTelemetry tracing and metrics support ([#8999](https://github.com/traefik/traefik/pull/8999) by [tomMoulard](https://github.com/tomMoulard)) +- **[middleware]** Disable Content-Type auto-detection by default ([#9546](https://github.com/traefik/traefik/pull/9546) by [sdelicata](https://github.com/sdelicata)) +- **[middleware]** Add gRPC-Web middleware ([#9451](https://github.com/traefik/traefik/pull/9451) by [juliens](https://github.com/juliens)) +- **[middleware]** Add support for Brotli ([#9387](https://github.com/traefik/traefik/pull/9387) by [glinton](https://github.com/glinton)) +- **[middleware]** Renaming IPWhiteList to IPAllowList ([#9457](https://github.com/traefik/traefik/pull/9457) by [wxmbugu](https://github.com/wxmbugu)) +- **[middleware,authentication,tracing]** Add captured headers options for tracing ([#10457](https://github.com/traefik/traefik/pull/10457) by [rtribotte](https://github.com/rtribotte)) +- **[middleware,metrics]** Semconv OTLP stable HTTP metrics ([#10421](https://github.com/traefik/traefik/pull/10421) by [mmatur](https://github.com/mmatur)) +- **[nomad]** Allow empty services ([#10375](https://github.com/traefik/traefik/pull/10375) by [chrispruitt](https://github.com/chrispruitt)) +- **[nomad]** Support multiple namespaces in the Nomad Provider ([#9332](https://github.com/traefik/traefik/pull/9332) by [0teh](https://github.com/0teh)) +- **[plugins]** Upgrade http-wasm host to v0.6.0 to support clients using v0.4.0 ([#10475](https://github.com/traefik/traefik/pull/10475) by [jcchavezs](https://github.com/jcchavezs)) +- **[rules]** Update routing syntax ([#9531](https://github.com/traefik/traefik/pull/9531) by [skwair](https://github.com/skwair)) +- **[server]** Rework servers load-balancer to use the WRR ([#9431](https://github.com/traefik/traefik/pull/9431) by [juliens](https://github.com/juliens)) +- **[server]** Allow default entrypoints definition ([#9100](https://github.com/traefik/traefik/pull/9100) by [applejag](https://github.com/applejag)) +- **[tls,service]** Support SPIFFE mTLS between Traefik and Backend servers ([#9394](https://github.com/traefik/traefik/pull/9394) by [jlevesy](https://github.com/jlevesy)) +- **[tls]** Add Tailscale certificate resolver ([#9237](https://github.com/traefik/traefik/pull/9237) by [kevinpollet](https://github.com/kevinpollet)) +- **[tls]** Support SNI routing with Postgres STARTTLS connections ([#9377](https://github.com/traefik/traefik/pull/9377) by [rtribotte](https://github.com/rtribotte)) +- **[tracing]** Support OTEL_PROPAGATORS to configure tracing propagation ([#10465](https://github.com/traefik/traefik/pull/10465) by [youkoulayley](https://github.com/youkoulayley)) +- **[webui,middleware,k8s/gatewayapi]** Support RequestHeaderModifier filter ([#10521](https://github.com/traefik/traefik/pull/10521) by [rtribotte](https://github.com/rtribotte)) +- Remove deprecated options ([#9527](https://github.com/traefik/traefik/pull/9527) by [sdelicata](https://github.com/sdelicata)) + + **Bug fixes:** +- **[docker]** Fix struct names in comment ([#10503](https://github.com/traefik/traefik/pull/10503) by [hishope](https://github.com/hishope)) +- **[k8s/crd,k8s]** Adds the missing circuit-breaker response code for CRD ([#10625](https://github.com/traefik/traefik/pull/10625) by [ldez](https://github.com/ldez)) +- **[logs]** Avoid cumulative send anonymous usage log ([#10579](https://github.com/traefik/traefik/pull/10579) by [mmatur](https://github.com/mmatur)) +- **[logs]** Change traefik cmd error log to error level ([#9569](https://github.com/traefik/traefik/pull/9569) by [tomMoulard](https://github.com/tomMoulard)) +- **[logs]** Fix log level ([#9545](https://github.com/traefik/traefik/pull/9545) by [ldez](https://github.com/ldez)) +- **[metrics]** Fix ServerUp metric ([#9534](https://github.com/traefik/traefik/pull/9534) by [kevinpollet](https://github.com/kevinpollet)) +- **[rules]** Rework Host and HostRegexp matchers ([#9559](https://github.com/traefik/traefik/pull/9559) by [tomMoulard](https://github.com/tomMoulard)) +- **[rules]** Support regexp in path/pathprefix in matcher v2 ([#10546](https://github.com/traefik/traefik/pull/10546) by [youkoulayley](https://github.com/youkoulayley)) +- **[tls,service]** Enforce default servers transport SPIFFE config ([#9444](https://github.com/traefik/traefik/pull/9444) by [jlevesy](https://github.com/jlevesy)) +- **[webui]** Add missing Docker Swarm logo ([#10529](https://github.com/traefik/traefik/pull/10529) by [ldez](https://github.com/ldez)) +- Fix a regression on flags using spaces between key and value ([#10445](https://github.com/traefik/traefik/pull/10445) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[k8s,k8s/gatewayapi]** Add ReferenceGrants to Gateway API Traefik controller RBAC ([#10462](https://github.com/traefik/traefik/pull/10462) by [rtribotte](https://github.com/rtribotte)) +- **[k8s]** Update Kubernetes version for v3 Helm chart ([#10637](https://github.com/traefik/traefik/pull/10637) by [jnoordsij](https://github.com/jnoordsij)) +- **[k8s]** Fix invalid version in docs about Gateway API on Traefik v3 ([#10474](https://github.com/traefik/traefik/pull/10474) by [mloiseleur](https://github.com/mloiseleur)) +- **[rules]** Improve ruleSyntax option documentation ([#10441](https://github.com/traefik/traefik/pull/10441) by [rtribotte](https://github.com/rtribotte)) +- Fix some typos in comments ([#10626](https://github.com/traefik/traefik/pull/10626) by [hidewrong](https://github.com/hidewrong)) +- Prepare release v3.0.0-rc5 ([#10605](https://github.com/traefik/traefik/pull/10605) by [ldez](https://github.com/ldez)) +- Prepare release v3.0.0 rc3 ([#10520](https://github.com/traefik/traefik/pull/10520) by [rtribotte](https://github.com/rtribotte)) +- Prepare release v3.0.0-rc2 ([#10514](https://github.com/traefik/traefik/pull/10514) by [rtribotte](https://github.com/rtribotte)) +- Fix typo in migration docs ([#10478](https://github.com/traefik/traefik/pull/10478) by [Eisberge](https://github.com/Eisberge)) +- Prepare release v3.0.0-rc4 ([#10588](https://github.com/traefik/traefik/pull/10588) by [kevinpollet](https://github.com/kevinpollet)) +- Fix typo and improve explanation on internal resources ([#10563](https://github.com/traefik/traefik/pull/10563) by [mloiseleur](https://github.com/mloiseleur)) +- Fix typo in dialer_test.go ([#10552](https://github.com/traefik/traefik/pull/10552) by [eltociear](https://github.com/eltociear)) +- Prepare release v3.0.0-beta2 ([#9587](https://github.com/traefik/traefik/pull/9587) by [tomMoulard](https://github.com/tomMoulard)) +- Prepare release v3.0.0-beta1 ([#9577](https://github.com/traefik/traefik/pull/9577) by [rtribotte](https://github.com/rtribotte)) + +**Misc:** +- Merge current v2.11 into v3.0 ([#10651](https://github.com/traefik/traefik/pull/10651) by [ldez](https://github.com/ldez)) +- Merge current v2.11 into v3.0 ([#10632](https://github.com/traefik/traefik/pull/10632) by [kevinpollet](https://github.com/kevinpollet)) +- Merge current v2.11 into v3.0 ([#10604](https://github.com/traefik/traefik/pull/10604) by [ldez](https://github.com/ldez)) +- Merge branch v2.11 into v3.0 ([#10587](https://github.com/traefik/traefik/pull/10587) by [kevinpollet](https://github.com/kevinpollet)) +- Merge current v2.11 into v3.0 ([#10566](https://github.com/traefik/traefik/pull/10566) by [mmatur](https://github.com/mmatur)) +- Merge current v2.11 into v3.0 ([#10564](https://github.com/traefik/traefik/pull/10564) by [ldez](https://github.com/ldez)) +- Merge branch v2.11 into v3.0 ([#10519](https://github.com/traefik/traefik/pull/10519) by [rtribotte](https://github.com/rtribotte)) +- Merge v2.11 into v3.0 ([#10513](https://github.com/traefik/traefik/pull/10513) by [mmatur](https://github.com/mmatur)) +- Merge v3.0 into master ([#10418](https://github.com/traefik/traefik/pull/10418) by [mmatur](https://github.com/mmatur)) +- Merge current v3.0 into master ([#10655](https://github.com/traefik/traefik/pull/10655) by [ldez](https://github.com/ldez)) +- Merge current v3.0 into master ([#10567](https://github.com/traefik/traefik/pull/10567) by [ldez](https://github.com/ldez)) +- Merge v3.0 into master ([#10418](https://github.com/traefik/traefik/pull/10418) by [mmatur](https://github.com/mmatur)) +- Merge current v3.0 into master ([#10040](https://github.com/traefik/traefik/pull/10040) by [mmatur](https://github.com/mmatur)) +- Merge current v3.0 into master ([#9933](https://github.com/traefik/traefik/pull/9933) by [ldez](https://github.com/ldez)) +- Merge current v3.0 into master ([#9897](https://github.com/traefik/traefik/pull/9897) by [ldez](https://github.com/ldez)) +- Merge current v3.0 into master ([#9871](https://github.com/traefik/traefik/pull/9871) by [ldez](https://github.com/ldez)) +- Merge current v3.0 into master ([#9807](https://github.com/traefik/traefik/pull/9807) by [ldez](https://github.com/ldez)) +- Merge current v2.9 into master ([#9586](https://github.com/traefik/traefik/pull/9586) by [tomMoulard](https://github.com/tomMoulard)) +- Merge current v2.9 into master ([#9576](https://github.com/traefik/traefik/pull/9576) by [rtribotte](https://github.com/rtribotte)) +- Merge branch v2.9 into master ([#9554](https://github.com/traefik/traefik/pull/9554) by [ldez](https://github.com/ldez)) +- Merge branch v2.9 into master ([#9536](https://github.com/traefik/traefik/pull/9536) by [ldez](https://github.com/ldez)) +- Merge branch v2.9 into master ([#9532](https://github.com/traefik/traefik/pull/9532) by [ldez](https://github.com/ldez)) +- Merge branch v2.9 into master ([#9482](https://github.com/traefik/traefik/pull/9482) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v2.9 into master ([#9464](https://github.com/traefik/traefik/pull/9464) by [ldez](https://github.com/ldez)) +- Merge branch v2.9 into master ([#9449](https://github.com/traefik/traefik/pull/9449) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v2.9 into master ([#9419](https://github.com/traefik/traefik/pull/9419) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v2.9 into master ([#9351](https://github.com/traefik/traefik/pull/9351) by [rtribotte](https://github.com/rtribotte)) + ## [v3.0.0-rc5](https://github.com/traefik/traefik/tree/v3.0.0-rc4) (2024-04-11) [All Commits](https://github.com/traefik/traefik/compare/v3.0.0-rc4...v3.0.0-rc5) diff --git a/script/gcg/traefik-final-release-part1.toml b/script/gcg/traefik-final-release-part1.toml index 2e73738fb..5b0baaf97 100644 --- a/script/gcg/traefik-final-release-part1.toml +++ b/script/gcg/traefik-final-release-part1.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example final release of v2.11.0 -CurrentRef = "v2.11" -PreviousRef = "v2.11.0-rc1" -BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.0" +# example final release of v3.0.0 +CurrentRef = "v3.0" +PreviousRef = "v3.0.0-beta1" +BaseBranch = "v3.0" +FutureCurrentRefName = "v3.0.0" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 diff --git a/script/gcg/traefik-final-release-part2.toml b/script/gcg/traefik-final-release-part2.toml index bfd11e0f1..f956aaea7 100644 --- a/script/gcg/traefik-final-release-part2.toml +++ b/script/gcg/traefik-final-release-part2.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example final release of v2.11.0 -CurrentRef = "v2.11.0-rc1" -PreviousRef = "v2.10.0-rc1" +# example final release of v3.0.0 +CurrentRef = "v3.0.0-beta1" +PreviousRef = "v2.9.0-rc1" BaseBranch = "master" -FutureCurrentRefName = "v2.11.0-rc1" +FutureCurrentRefName = "v3.0.0-beta1" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 From b0d19bd4663d8340c57d833ba1a4ae39251d8c27 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Tue, 30 Apr 2024 02:20:04 +0200 Subject: [PATCH 05/26] Bump tscert dependency to 28a91b69a046 Co-authored-by: Romain --- go.mod | 2 +- go.sum | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d6c253a4c..6e12e68e9 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/spiffe/go-spiffe/v2 v2.1.1 github.com/stretchr/testify v1.9.0 github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154 - github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 + github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 github.com/testcontainers/testcontainers-go v0.30.0 github.com/testcontainers/testcontainers-go/modules/k3s v0.30.0 github.com/tetratelabs/wazero v1.5.0 diff --git a/go.sum b/go.sum index a0b0011f5..2e610a138 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,7 @@ github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= @@ -1072,8 +1073,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154 h1:XGopsea1Dw7ecQ8JscCNQXDGYAKDiWjDeXnpN/+BY9g= github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 h1:xwMw7LFhV9dbvot9A7NLClP9udqbjrQlIwWMH8e7uiQ= -github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2/go.mod h1:hL4gB6APAasMR2NNi/JHzqKkxW3EPQlFgLEq9PMi2t0= +github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 h1:8rUlviSVOEe7TMk7W0gIPrW8MqEzYfZHpsNWSf8s2vg= +github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz27tVi2r70JYnm5y0Zk8w0Qzsx+vfUw3oqSyrEfP8= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg= @@ -1410,7 +1411,6 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From aee515b93079f59bd3083e8f1a5850ed7b1a0ff3 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 2 May 2024 18:42:03 +0200 Subject: [PATCH 06/26] Regenerate v3.0.0 changelog --- CHANGELOG.md | 110 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7045623db..52822c031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,21 +3,38 @@ **Enhancements:** - **[consul]** ConsulCatalog StrictChecks ([#10388](https://github.com/traefik/traefik/pull/10388) by [djenriquez](https://github.com/djenriquez)) +- **[docker,docker/swarm]** Split Docker provider ([#9652](https://github.com/traefik/traefik/pull/9652) by [ldez](https://github.com/ldez)) +- **[docker,service]** Adds weight on ServersLoadBalancer ([#10372](https://github.com/traefik/traefik/pull/10372) by [juliens](https://github.com/juliens)) - **[ecs]** Add option to keep only healthy ECS tasks ([#8027](https://github.com/traefik/traefik/pull/8027) by [Michampt](https://github.com/Michampt)) +- **[file]** Reload provider file configuration on SIGHUP ([#9993](https://github.com/traefik/traefik/pull/9993) by [sokoide](https://github.com/sokoide)) - **[healthcheck]** Support gRPC healthcheck ([#8583](https://github.com/traefik/traefik/pull/8583) by [jjacque](https://github.com/jjacque)) - **[healthcheck]** Add a status option to the service health check ([#9463](https://github.com/traefik/traefik/pull/9463) by [guoard](https://github.com/guoard)) - **[http]** Support custom headers when fetching configuration through HTTP ([#9421](https://github.com/traefik/traefik/pull/9421) by [kevinpollet](https://github.com/kevinpollet)) - **[http3]** Moves HTTP/3 outside the experimental section ([#9570](https://github.com/traefik/traefik/pull/9570) by [sdelicata](https://github.com/sdelicata)) +- **[k8s,hub]** Remove deprecated code ([#9804](https://github.com/traefik/traefik/pull/9804) by [ldez](https://github.com/ldez)) +- **[k8s,k8s/gatewayapi]** Support for cross-namespace references / GatewayAPI ReferenceGrants ([#10346](https://github.com/traefik/traefik/pull/10346) by [pascal-hofmann](https://github.com/pascal-hofmann)) +- **[k8s,k8s/gatewayapi]** Support HostSNIRegexp in GatewayAPI TLS routes ([#9486](https://github.com/traefik/traefik/pull/9486) by [ddtmachado](https://github.com/ddtmachado)) +- **[k8s,k8s/gatewayapi]** Upgrade gateway api to v1.0.0 ([#10205](https://github.com/traefik/traefik/pull/10205) by [mmatur](https://github.com/mmatur)) +- **[k8s/crd,k8s]** Support file path as input param for Kubernetes token value ([#10232](https://github.com/traefik/traefik/pull/10232) by [sssash18](https://github.com/sssash18)) - **[k8s/gatewayapi]** Add option to set Gateway status address ([#10582](https://github.com/traefik/traefik/pull/10582) by [kevinpollet](https://github.com/kevinpollet)) - **[k8s/gatewayapi]** Toggle support for experimental channel ([#10435](https://github.com/traefik/traefik/pull/10435) by [SantoDE](https://github.com/SantoDE)) +- **[k8s/gatewayapi]** Add option to set Gateway status address ([#10582](https://github.com/traefik/traefik/pull/10582) by [kevinpollet](https://github.com/kevinpollet)) +- **[k8s/gatewayapi]** Add support for HTTPRequestRedirectFilter in k8s Gateway API ([#9408](https://github.com/traefik/traefik/pull/9408) by [romantomjak](https://github.com/romantomjak)) - **[k8s/gatewayapi]** Handle middlewares in filters extension reference ([#10511](https://github.com/traefik/traefik/pull/10511) by [youkoulayley](https://github.com/youkoulayley)) - **[k8s/ingress,k8s/crd,k8s,k8s/gatewayapi]** Use runtime.Object in routerTransform ([#10523](https://github.com/traefik/traefik/pull/10523) by [juliens](https://github.com/juliens)) -- **[k8s/crd,k8s]** Allow to use internal node IPs for NodePort services ([#10278](https://github.com/traefik/traefik/pull/10278) by [jorisvergeer](https://github.com/jorisvergeer)) +- **[k8s/ingress,k8s]** Add option to the Ingress provider to disable IngressClass lookup ([#9281](https://github.com/traefik/traefik/pull/9281) by [jandillenkofer](https://github.com/jandillenkofer)) +- **[k8s/ingress,k8s]** Remove support of the networking.k8s.io/v1beta1 APIVersion ([#9949](https://github.com/traefik/traefik/pull/9949) by [rtribotte](https://github.com/rtribotte)) +- **[logs]** Introduce static config hints ([#10351](https://github.com/traefik/traefik/pull/10351) by [rtribotte](https://github.com/rtribotte)) - **[logs,performance]** New logger for the Traefik logs ([#9515](https://github.com/traefik/traefik/pull/9515) by [ldez](https://github.com/ldez)) - **[logs,plugins]** Retry on plugin API calls ([#9530](https://github.com/traefik/traefik/pull/9530) by [ldez](https://github.com/ldez)) - **[logs,provider]** Improve provider logs ([#9562](https://github.com/traefik/traefik/pull/9562) by [ldez](https://github.com/ldez)) - **[logs]** Improve test logger assertions ([#9533](https://github.com/traefik/traefik/pull/9533) by [ldez](https://github.com/ldez)) +- **[marathon]** Remove Marathon provider ([#9614](https://github.com/traefik/traefik/pull/9614) by [rtribotte](https://github.com/rtribotte)) +- **[metrics,tracing,accesslogs]** Remove observability for internal resources ([#9633](https://github.com/traefik/traefik/pull/9633) by [rtribotte](https://github.com/rtribotte)) - **[metrics,tracing]** Upgrade opentelemetry dependencies ([#10472](https://github.com/traefik/traefik/pull/10472) by [mmatur](https://github.com/mmatur)) +- **[metrics]** Add support for sending DogStatsD metrics over Unix Socket ([#10199](https://github.com/traefik/traefik/pull/10199) by [liamvdv](https://github.com/liamvdv)) +- **[metrics]** Remove InfluxDB v1 metrics middleware ([#9612](https://github.com/traefik/traefik/pull/9612) by [tomMoulard](https://github.com/tomMoulard)) +- **[metrics]** Upgrade OpenTelemetry dependencies ([#10181](https://github.com/traefik/traefik/pull/10181) by [mmatur](https://github.com/mmatur)) - **[metrics]** Support gRPC and gRPC-Web protocol in metrics ([#9483](https://github.com/traefik/traefik/pull/9483) by [longit644](https://github.com/longit644)) - **[middleware,accesslogs]** Log TLS client subject ([#9285](https://github.com/traefik/traefik/pull/9285) by [xmessi](https://github.com/xmessi)) - **[middleware,metrics,tracing,otel]** Add OpenTelemetry tracing and metrics support ([#8999](https://github.com/traefik/traefik/pull/8999) by [tomMoulard](https://github.com/tomMoulard)) @@ -26,46 +43,96 @@ - **[middleware]** Add support for Brotli ([#9387](https://github.com/traefik/traefik/pull/9387) by [glinton](https://github.com/glinton)) - **[middleware]** Renaming IPWhiteList to IPAllowList ([#9457](https://github.com/traefik/traefik/pull/9457) by [wxmbugu](https://github.com/wxmbugu)) - **[middleware,authentication,tracing]** Add captured headers options for tracing ([#10457](https://github.com/traefik/traefik/pull/10457) by [rtribotte](https://github.com/rtribotte)) +- **[middleware,authentication]** Add forwardAuth.addAuthCookiesToResponse ([#8924](https://github.com/traefik/traefik/pull/8924) by [tgunsch](https://github.com/tgunsch)) - **[middleware,metrics]** Semconv OTLP stable HTTP metrics ([#10421](https://github.com/traefik/traefik/pull/10421) by [mmatur](https://github.com/mmatur)) +- **[middleware]** Feat re introduce IpWhitelist middleware as deprecated ([#10341](https://github.com/traefik/traefik/pull/10341) by [mmatur](https://github.com/mmatur)) +- **[middleware]** Disable br compression when no Accept-Encoding header is present ([#10178](https://github.com/traefik/traefik/pull/10178) by [robin-moser](https://github.com/robin-moser)) +- **[middleware]** Implements the includedContentTypes option for the compress middleware ([#10207](https://github.com/traefik/traefik/pull/10207) by [rjsocha](https://github.com/rjsocha)) +- **[middleware]** Add `rejectStatusCode` option to `IPAllowList` middleware ([#10130](https://github.com/traefik/traefik/pull/10130) by [jfly](https://github.com/jfly)) +- **[middleware]** Merge v2.11 into v3.0 ([#10426](https://github.com/traefik/traefik/pull/10426) by [mmatur](https://github.com/mmatur)) +- **[middleware]** Add ResponseCode to CircuitBreaker ([#10147](https://github.com/traefik/traefik/pull/10147) by [fahhem](https://github.com/fahhem)) - **[nomad]** Allow empty services ([#10375](https://github.com/traefik/traefik/pull/10375) by [chrispruitt](https://github.com/chrispruitt)) - **[nomad]** Support multiple namespaces in the Nomad Provider ([#9332](https://github.com/traefik/traefik/pull/9332) by [0teh](https://github.com/0teh)) +- **[plugins]** Add http-wasm plugin support to Traefik ([#10189](https://github.com/traefik/traefik/pull/10189) by [zetaab](https://github.com/zetaab)) - **[plugins]** Upgrade http-wasm host to v0.6.0 to support clients using v0.4.0 ([#10475](https://github.com/traefik/traefik/pull/10475) by [jcchavezs](https://github.com/jcchavezs)) +- **[rancher]** Remove Rancher v1 provider ([#9613](https://github.com/traefik/traefik/pull/9613) by [tomMoulard](https://github.com/tomMoulard)) +- **[rules]** Bring back v2 rule matchers ([#10339](https://github.com/traefik/traefik/pull/10339) by [rtribotte](https://github.com/rtribotte)) +- **[rules]** Remove containous/mux from HTTP muxer ([#9558](https://github.com/traefik/traefik/pull/9558) by [tomMoulard](https://github.com/tomMoulard)) - **[rules]** Update routing syntax ([#9531](https://github.com/traefik/traefik/pull/9531) by [skwair](https://github.com/skwair)) +- **[server]** Add SO_REUSEPORT support for EntryPoints ([#9834](https://github.com/traefik/traefik/pull/9834) by [aofei](https://github.com/aofei)) - **[server]** Rework servers load-balancer to use the WRR ([#9431](https://github.com/traefik/traefik/pull/9431) by [juliens](https://github.com/juliens)) - **[server]** Allow default entrypoints definition ([#9100](https://github.com/traefik/traefik/pull/9100) by [applejag](https://github.com/applejag)) +- **[sticky-session]** Support setting sticky cookie max age ([#10176](https://github.com/traefik/traefik/pull/10176) by [Patrick0308](https://github.com/Patrick0308)) +- **[tls,tcp,service]** Add TCP Servers Transports support ([#9465](https://github.com/traefik/traefik/pull/9465) by [sdelicata](https://github.com/sdelicata)) - **[tls,service]** Support SPIFFE mTLS between Traefik and Backend servers ([#9394](https://github.com/traefik/traefik/pull/9394) by [jlevesy](https://github.com/jlevesy)) - **[tls]** Add Tailscale certificate resolver ([#9237](https://github.com/traefik/traefik/pull/9237) by [kevinpollet](https://github.com/kevinpollet)) - **[tls]** Support SNI routing with Postgres STARTTLS connections ([#9377](https://github.com/traefik/traefik/pull/9377) by [rtribotte](https://github.com/rtribotte)) +- **[tracing,otel]** Migrate to opentelemetry ([#10223](https://github.com/traefik/traefik/pull/10223) by [zetaab](https://github.com/zetaab)) - **[tracing]** Support OTEL_PROPAGATORS to configure tracing propagation ([#10465](https://github.com/traefik/traefik/pull/10465) by [youkoulayley](https://github.com/youkoulayley)) - **[webui,middleware,k8s/gatewayapi]** Support RequestHeaderModifier filter ([#10521](https://github.com/traefik/traefik/pull/10521) by [rtribotte](https://github.com/rtribotte)) +- **[webui]** Added router priority to webui's list and detail page ([#9004](https://github.com/traefik/traefik/pull/9004) by [bendre90](https://github.com/bendre90)) +- Reintroduce dropped v2 dynamic config ([#10355](https://github.com/traefik/traefik/pull/10355) by [rtribotte](https://github.com/rtribotte)) - Remove deprecated options ([#9527](https://github.com/traefik/traefik/pull/9527) by [sdelicata](https://github.com/sdelicata)) - **Bug fixes:** +**Bug fixes:** +- **[consul,tls]** Enable TLS for Consul Connect TCP services ([#10140](https://github.com/traefik/traefik/pull/10140) by [rtribotte](https://github.com/rtribotte)) - **[docker]** Fix struct names in comment ([#10503](https://github.com/traefik/traefik/pull/10503) by [hishope](https://github.com/hishope)) - **[k8s/crd,k8s]** Adds the missing circuit-breaker response code for CRD ([#10625](https://github.com/traefik/traefik/pull/10625) by [ldez](https://github.com/ldez)) +- **[k8s/crd,k8s]** Delete warning in Kubernetes CRD provider about the supported version ([#10414](https://github.com/traefik/traefik/pull/10414) by [nmengin](https://github.com/nmengin)) - **[logs]** Avoid cumulative send anonymous usage log ([#10579](https://github.com/traefik/traefik/pull/10579) by [mmatur](https://github.com/mmatur)) - **[logs]** Change traefik cmd error log to error level ([#9569](https://github.com/traefik/traefik/pull/9569) by [tomMoulard](https://github.com/tomMoulard)) - **[logs]** Fix log level ([#9545](https://github.com/traefik/traefik/pull/9545) by [ldez](https://github.com/ldez)) +- **[metrics]** Fix OpenTelemetry metrics ([#9962](https://github.com/traefik/traefik/pull/9962) by [rtribotte](https://github.com/rtribotte)) +- **[metrics]** Fix OpenTelemetry service name ([#9619](https://github.com/traefik/traefik/pull/9619) by [tomMoulard](https://github.com/tomMoulard)) +- **[metrics]** Fix open connections metric ([#9656](https://github.com/traefik/traefik/pull/9656) by [mpl](https://github.com/mpl)) +- **[metrics]** Remove config reload failure metrics ([#9660](https://github.com/traefik/traefik/pull/9660) by [rtribotte](https://github.com/rtribotte)) +- **[metrics]** Fix OpenTelemetry unit tests ([#10380](https://github.com/traefik/traefik/pull/10380) by [mmatur](https://github.com/mmatur)) - **[metrics]** Fix ServerUp metric ([#9534](https://github.com/traefik/traefik/pull/9534) by [kevinpollet](https://github.com/kevinpollet)) +- **[middleware,authentication,metrics,tracing]** Align OpenTelemetry tracing and metrics configurations ([#10404](https://github.com/traefik/traefik/pull/10404) by [rtribotte](https://github.com/rtribotte)) +- **[middleware]** Fix brotli response status code when compression is disabled ([#10396](https://github.com/traefik/traefik/pull/10396) by [rtribotte](https://github.com/rtribotte)) +- **[middleware]** Allow short healthcheck interval with long timeout ([#9832](https://github.com/traefik/traefik/pull/9832) by [kevinmcconnell](https://github.com/kevinmcconnell)) +- **[middleware]** Fix GrpcWeb middleware to clear ContentLength after translating to normal gRPC message ([#9782](https://github.com/traefik/traefik/pull/9782) by [CleverUnderDog](https://github.com/CleverUnderDog)) +- **[provider,tls]** Bump tscert dependency to 28a91b69a046 ([#10668](https://github.com/traefik/traefik/pull/10668) by [kevinpollet](https://github.com/kevinpollet)) - **[rules]** Rework Host and HostRegexp matchers ([#9559](https://github.com/traefik/traefik/pull/9559) by [tomMoulard](https://github.com/tomMoulard)) - **[rules]** Support regexp in path/pathprefix in matcher v2 ([#10546](https://github.com/traefik/traefik/pull/10546) by [youkoulayley](https://github.com/youkoulayley)) +- **[sticky-session,server]** Set sameSite field for wrr load balancer sticky cookie ([#10066](https://github.com/traefik/traefik/pull/10066) by [sunyakun](https://github.com/sunyakun)) +- **[tcp]** Don't log EOF or timeout errors while peeking first bytes in Postgres StartTLS hook ([#9663](https://github.com/traefik/traefik/pull/9663) by [rtribotte](https://github.com/rtribotte)) +- **[tls,server]** Compute priority for https forwarder TLS routes ([#10288](https://github.com/traefik/traefik/pull/10288) by [rtribotte](https://github.com/rtribotte)) - **[tls,service]** Enforce default servers transport SPIFFE config ([#9444](https://github.com/traefik/traefik/pull/9444) by [jlevesy](https://github.com/jlevesy)) +- **[webui]** Detect dashboard assets content types ([#9622](https://github.com/traefik/traefik/pull/9622) by [tomMoulard](https://github.com/tomMoulard)) - **[webui]** Add missing Docker Swarm logo ([#10529](https://github.com/traefik/traefik/pull/10529) by [ldez](https://github.com/ldez)) +- **[webui]** fix: detect dashboard content types ([#9594](https://github.com/traefik/traefik/pull/9594) by [ldez](https://github.com/ldez)) - Fix a regression on flags using spaces between key and value ([#10445](https://github.com/traefik/traefik/pull/10445) by [ldez](https://github.com/ldez)) **Documentation:** +- **[docker/swarm]** Remove documentation of old swarm options ([#10001](https://github.com/traefik/traefik/pull/10001) by [ldez](https://github.com/ldez)) +- **[docker/swarm]** Fix minor typo in swarm example ([#10071](https://github.com/traefik/traefik/pull/10071) by [kaznovac](https://github.com/kaznovac)) - **[k8s,k8s/gatewayapi]** Add ReferenceGrants to Gateway API Traefik controller RBAC ([#10462](https://github.com/traefik/traefik/pull/10462) by [rtribotte](https://github.com/rtribotte)) - **[k8s]** Update Kubernetes version for v3 Helm chart ([#10637](https://github.com/traefik/traefik/pull/10637) by [jnoordsij](https://github.com/jnoordsij)) +- **[k8s]** Improve Kubernetes support documentation ([#9974](https://github.com/traefik/traefik/pull/9974) by [rtribotte](https://github.com/rtribotte)) - **[k8s]** Fix invalid version in docs about Gateway API on Traefik v3 ([#10474](https://github.com/traefik/traefik/pull/10474) by [mloiseleur](https://github.com/mloiseleur)) - **[rules]** Improve ruleSyntax option documentation ([#10441](https://github.com/traefik/traefik/pull/10441) by [rtribotte](https://github.com/rtribotte)) -- Fix some typos in comments ([#10626](https://github.com/traefik/traefik/pull/10626) by [hidewrong](https://github.com/hidewrong)) -- Prepare release v3.0.0-rc5 ([#10605](https://github.com/traefik/traefik/pull/10605) by [ldez](https://github.com/ldez)) -- Prepare release v3.0.0 rc3 ([#10520](https://github.com/traefik/traefik/pull/10520) by [rtribotte](https://github.com/rtribotte)) +- Prepare release v3.0.0 ([#10666](https://github.com/traefik/traefik/pull/10666) by [rtribotte](https://github.com/rtribotte)) - Prepare release v3.0.0-rc2 ([#10514](https://github.com/traefik/traefik/pull/10514) by [rtribotte](https://github.com/rtribotte)) - Fix typo in migration docs ([#10478](https://github.com/traefik/traefik/pull/10478) by [Eisberge](https://github.com/Eisberge)) -- Prepare release v3.0.0-rc4 ([#10588](https://github.com/traefik/traefik/pull/10588) by [kevinpollet](https://github.com/kevinpollet)) -- Fix typo and improve explanation on internal resources ([#10563](https://github.com/traefik/traefik/pull/10563) by [mloiseleur](https://github.com/mloiseleur)) +- Prepare release v3.0.0 rc3 ([#10520](https://github.com/traefik/traefik/pull/10520) by [rtribotte](https://github.com/rtribotte)) - Fix typo in dialer_test.go ([#10552](https://github.com/traefik/traefik/pull/10552) by [eltociear](https://github.com/eltociear)) +- Fix typo and improve explanation on internal resources ([#10563](https://github.com/traefik/traefik/pull/10563) by [mloiseleur](https://github.com/mloiseleur)) +- Prepare release v3.0.0-rc1 ([#10429](https://github.com/traefik/traefik/pull/10429) by [mmatur](https://github.com/mmatur)) +- Update version comment in quick-start.md ([#10383](https://github.com/traefik/traefik/pull/10383) by [matthieuwerner](https://github.com/matthieuwerner)) +- Improve migration guide ([#10319](https://github.com/traefik/traefik/pull/10319) by [rtribotte](https://github.com/rtribotte)) +- Prepare release v3.0.0 beta5 ([#10273](https://github.com/traefik/traefik/pull/10273) by [rtribotte](https://github.com/rtribotte)) +- Prepare release v3.0.0-beta4 ([#10165](https://github.com/traefik/traefik/pull/10165) by [mmatur](https://github.com/mmatur)) +- Prepare release v3.0.0-rc4 ([#10588](https://github.com/traefik/traefik/pull/10588) by [kevinpollet](https://github.com/kevinpollet)) +- Fix bad anchor on documentation ([#10041](https://github.com/traefik/traefik/pull/10041) by [mmatur](https://github.com/mmatur)) +- Prepare release v3.0.0-rc5 ([#10605](https://github.com/traefik/traefik/pull/10605) by [ldez](https://github.com/ldez)) +- Fix migration guide heading ([#9989](https://github.com/traefik/traefik/pull/9989) by [ldez](https://github.com/ldez)) +- Prepare release v3.0.0-beta3 ([#9978](https://github.com/traefik/traefik/pull/9978) by [ldez](https://github.com/ldez)) +- Fix some typos in comments ([#10626](https://github.com/traefik/traefik/pull/10626) by [hidewrong](https://github.com/hidewrong)) +- Adjust quick start ([#9790](https://github.com/traefik/traefik/pull/9790) by [svx](https://github.com/svx)) +- Mention PathPrefix matcher changes in V3 Migration Guide ([#9727](https://github.com/traefik/traefik/pull/9727) by [aofei](https://github.com/aofei)) +- Fix yaml indentation in the HTTP3 example ([#9724](https://github.com/traefik/traefik/pull/9724) by [benwaffle](https://github.com/benwaffle)) +- Add OpenTelemetry in observability overview ([#9654](https://github.com/traefik/traefik/pull/9654) by [tomMoulard](https://github.com/tomMoulard)) - Prepare release v3.0.0-beta2 ([#9587](https://github.com/traefik/traefik/pull/9587) by [tomMoulard](https://github.com/tomMoulard)) - Prepare release v3.0.0-beta1 ([#9577](https://github.com/traefik/traefik/pull/9577) by [rtribotte](https://github.com/rtribotte)) @@ -78,16 +145,25 @@ - Merge current v2.11 into v3.0 ([#10564](https://github.com/traefik/traefik/pull/10564) by [ldez](https://github.com/ldez)) - Merge branch v2.11 into v3.0 ([#10519](https://github.com/traefik/traefik/pull/10519) by [rtribotte](https://github.com/rtribotte)) - Merge v2.11 into v3.0 ([#10513](https://github.com/traefik/traefik/pull/10513) by [mmatur](https://github.com/mmatur)) -- Merge v3.0 into master ([#10418](https://github.com/traefik/traefik/pull/10418) by [mmatur](https://github.com/mmatur)) -- Merge current v3.0 into master ([#10655](https://github.com/traefik/traefik/pull/10655) by [ldez](https://github.com/ldez)) -- Merge current v3.0 into master ([#10567](https://github.com/traefik/traefik/pull/10567) by [ldez](https://github.com/ldez)) -- Merge v3.0 into master ([#10418](https://github.com/traefik/traefik/pull/10418) by [mmatur](https://github.com/mmatur)) -- Merge current v3.0 into master ([#10040](https://github.com/traefik/traefik/pull/10040) by [mmatur](https://github.com/mmatur)) -- Merge current v3.0 into master ([#9933](https://github.com/traefik/traefik/pull/9933) by [ldez](https://github.com/ldez)) -- Merge current v3.0 into master ([#9897](https://github.com/traefik/traefik/pull/9897) by [ldez](https://github.com/ldez)) -- Merge current v3.0 into master ([#9871](https://github.com/traefik/traefik/pull/9871) by [ldez](https://github.com/ldez)) -- Merge current v3.0 into master ([#9807](https://github.com/traefik/traefik/pull/9807) by [ldez](https://github.com/ldez)) -- Merge current v2.9 into master ([#9586](https://github.com/traefik/traefik/pull/9586) by [tomMoulard](https://github.com/tomMoulard)) +- Merge v2.11 into v3.0 ([#10417](https://github.com/traefik/traefik/pull/10417) by [mmatur](https://github.com/mmatur)) +- Merge current v2.11 into v3.0 ([#10382](https://github.com/traefik/traefik/pull/10382) by [mmatur](https://github.com/mmatur)) +- Merge back v2.11 into v3.0 ([#10377](https://github.com/traefik/traefik/pull/10377) by [mmatur](https://github.com/mmatur)) +- Merge back v2.11 into v3.0 ([#10353](https://github.com/traefik/traefik/pull/10353) by [youkoulayley](https://github.com/youkoulayley)) +- Merge current v2.11 into v3.0 ([#10328](https://github.com/traefik/traefik/pull/10328) by [mmatur](https://github.com/mmatur)) +- Merge current v2.10 into v3.0 ([#10272](https://github.com/traefik/traefik/pull/10272) by [rtribotte](https://github.com/rtribotte)) +- Merge current v2.10 into v3.0 ([#10164](https://github.com/traefik/traefik/pull/10164) by [mmatur](https://github.com/mmatur)) +- Merge current v2.10 into v3.0 ([#10038](https://github.com/traefik/traefik/pull/10038) by [mmatur](https://github.com/mmatur)) +- Merge branch v2.10 into v3.0 ([#9977](https://github.com/traefik/traefik/pull/9977) by [ldez](https://github.com/ldez)) +- Merge branch v2.10 into v3.0 ([#9931](https://github.com/traefik/traefik/pull/9931) by [ldez](https://github.com/ldez)) +- Merge branch v2.10 into v3.0 ([#9896](https://github.com/traefik/traefik/pull/9896) by [ldez](https://github.com/ldez)) +- Merge branch v2.10 into v3.0 ([#9867](https://github.com/traefik/traefik/pull/9867) by [ldez](https://github.com/ldez)) +- Merge branch v2.10 into v3.0 ([#9850](https://github.com/traefik/traefik/pull/9850) by [ldez](https://github.com/ldez)) +- Merge branch v2.10 into v3.0 ([#9845](https://github.com/traefik/traefik/pull/9845) by [ldez](https://github.com/ldez)) +- Merge branch v2.10 into v3.0 ([#9803](https://github.com/traefik/traefik/pull/9803) by [ldez](https://github.com/ldez)) +- Merge branch v2.10 into v3.0 ([#9793](https://github.com/traefik/traefik/pull/9793) by [ldez](https://github.com/ldez)) +- Merge branch v2.9 into v3.0 ([#9722](https://github.com/traefik/traefik/pull/9722) by [rtribotte](https://github.com/rtribotte)) +- Merge branch v2.9 into v3.0 ([#9650](https://github.com/traefik/traefik/pull/9650) by [tomMoulard](https://github.com/tomMoulard)) +- Merge branch v2.9 into v3.0 ([#9632](https://github.com/traefik/traefik/pull/9632) by [kevinpollet](https://github.com/kevinpollet)) - Merge current v2.9 into master ([#9576](https://github.com/traefik/traefik/pull/9576) by [rtribotte](https://github.com/rtribotte)) - Merge branch v2.9 into master ([#9554](https://github.com/traefik/traefik/pull/9554) by [ldez](https://github.com/ldez)) - Merge branch v2.9 into master ([#9536](https://github.com/traefik/traefik/pull/9536) by [ldez](https://github.com/ldez)) From a4150409c8ebe504e5f592a57cd18807f49dff36 Mon Sep 17 00:00:00 2001 From: Yewolf Date: Mon, 6 May 2024 14:50:04 +0200 Subject: [PATCH 07/26] Add link to the new http3 config in migration --- docs/content/migration/v2-to-v3.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index 61a60d990..6f80b2b15 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -147,6 +147,7 @@ It is now unsupported and would prevent Traefik to start. ##### Remediation The `http3` option should be removed from the static configuration experimental section. +To configure `http3`, please checkout the [entrypoint configuration documentation](https://doc.traefik.io/traefik/v3.0/routing/entrypoints/#http3_1). ### Consul provider From 15973f5503d07817ee9e0cf16f6b550c3be91a51 Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 6 May 2024 15:46:04 +0200 Subject: [PATCH 08/26] Remove deadlines when handling PostgreSQL connections --- pkg/server/router/tcp/router.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index b4b7384d9..89096c7ca 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -123,6 +123,11 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { } if postgres { + // Remove read/write deadline and delegate this to underlying TCP server. + if err := conn.SetDeadline(time.Time{}); err != nil { + log.Error().Err(err).Msg("Error while setting deadline") + } + r.servePostgres(r.GetConn(conn, getPeeked(br))) return } From a4aad5ce5c4bb8680ec8913350b4a1eb9cce1f7f Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 13 May 2024 08:54:03 +0200 Subject: [PATCH 09/26] fix: router documentation example --- docs/content/routing/routers/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 9cec6aaee..4ba434771 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -827,7 +827,7 @@ http: ``` !!! info "Multiple Hosts in a Rule" - The rule ```Host(`test1.example.com`,`test2.example.com`)``` will request a certificate with the main domain `test1.example.com` and SAN `test2.example.com`. + The rule ```Host(`test1.example.com`) || Host(`test2.example.com`)``` will request a certificate with the main domain `test1.example.com` and SAN `test2.example.com`. #### `domains` From d8cf90dadef4d882f8367943c8ffeccebf815fc0 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Mon, 13 May 2024 15:42:04 +0200 Subject: [PATCH 10/26] Improve mirroring example on Kubernetes --- .../routing/providers/kubernetes-crd.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index 9d1f5f721..20e67ce2a 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -897,15 +897,15 @@ More information in the dedicated [mirroring](../services/index.md#mirroring-ser spec: mirroring: - name: svc1 + name: svc1 # svc1 receives 100% of the traffic port: 80 mirrors: - - name: svc2 + - name: svc2 # svc2 receives a copy of 20% of this traffic port: 80 percent: 20 - - name: svc3 + - name: svc3 # svc3 receives a copy of 15% of this traffic kind: TraefikService - percent: 20 + percent: 15 ``` ```yaml tab="Mirroring Traefik Service" @@ -918,15 +918,15 @@ More information in the dedicated [mirroring](../services/index.md#mirroring-ser spec: mirroring: - name: wrr1 + name: wrr1 # wrr1 receives 100% of the traffic kind: TraefikService - mirrors: - - name: svc2 - port: 80 - percent: 20 - - name: svc3 - kind: TraefikService - percent: 20 + mirrors: + - name: svc2 # svc2 receives a copy of 20% of this traffic + port: 80 + percent: 20 + - name: svc3 # svc3 receives a copy of 10% of this traffic + kind: TraefikService + percent: 10 ``` ```yaml tab="K8s Service" From d8a778b5cdeefd83a4e6cd5c6e37b36dc4669a60 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 13 May 2024 15:44:03 +0200 Subject: [PATCH 11/26] Fix log.compress value --- docs/content/observability/logs.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/observability/logs.md b/docs/content/observability/logs.md index 5e22aa64d..2009aeee7 100644 --- a/docs/content/observability/logs.md +++ b/docs/content/observability/logs.md @@ -169,14 +169,14 @@ The default is not to perform compression. ```yaml tab="File (YAML)" log: - compress: 3 + compress: true ``` ```toml tab="File (TOML)" [log] - compress = 3 + compress = true ``` ```bash tab="CLI" ---log.compress=3 +--log.compress=true ``` From c2c1c3e09e2bcf41440eb823c01e70abe0548cb6 Mon Sep 17 00:00:00 2001 From: Landry Benguigui Date: Tue, 14 May 2024 09:42:04 +0200 Subject: [PATCH 12/26] Fix the rule syntax mechanism for TCP --- pkg/server/aggregator.go | 83 ++++++++++++++++++----------------- pkg/server/aggregator_test.go | 44 +++++++++++++++++++ 2 files changed, 87 insertions(+), 40 deletions(-) diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 75ad759cf..f015cb43d 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -84,6 +84,9 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint for serviceName, service := range configuration.TCP.Services { conf.TCP.Services[provider.MakeQualifiedName(pvd, serviceName)] = service } + for modelName, model := range configuration.TCP.Models { + conf.TCP.Models[provider.MakeQualifiedName(pvd, modelName)] = model + } for serversTransportName, serversTransport := range configuration.TCP.ServersTransports { conf.TCP.ServersTransports[provider.MakeQualifiedName(pvd, serversTransportName)] = serversTransport } @@ -146,60 +149,58 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint } func applyModel(cfg dynamic.Configuration) dynamic.Configuration { - if cfg.HTTP == nil || len(cfg.HTTP.Models) == 0 { - return cfg - } + if cfg.HTTP != nil && len(cfg.HTTP.Models) > 0 { + rts := make(map[string]*dynamic.Router) - rts := make(map[string]*dynamic.Router) + for name, rt := range cfg.HTTP.Routers { + router := rt.DeepCopy() - for name, rt := range cfg.HTTP.Routers { - router := rt.DeepCopy() + if !router.DefaultRule && router.RuleSyntax == "" { + for _, model := range cfg.HTTP.Models { + router.RuleSyntax = model.DefaultRuleSyntax + break + } + } - if !router.DefaultRule && router.RuleSyntax == "" { - for _, model := range cfg.HTTP.Models { - router.RuleSyntax = model.DefaultRuleSyntax - break + eps := router.EntryPoints + router.EntryPoints = nil + + for _, epName := range eps { + m, ok := cfg.HTTP.Models[epName+"@internal"] + if ok { + cp := router.DeepCopy() + + cp.EntryPoints = []string{epName} + + if cp.TLS == nil { + cp.TLS = m.TLS + } + + cp.Middlewares = append(m.Middlewares, cp.Middlewares...) + + rtName := name + if len(eps) > 1 { + rtName = epName + "-" + name + } + rts[rtName] = cp + } else { + router.EntryPoints = append(router.EntryPoints, epName) + + rts[name] = router + } } } - eps := router.EntryPoints - router.EntryPoints = nil - - for _, epName := range eps { - m, ok := cfg.HTTP.Models[epName+"@internal"] - if ok { - cp := router.DeepCopy() - - cp.EntryPoints = []string{epName} - - if cp.TLS == nil { - cp.TLS = m.TLS - } - - cp.Middlewares = append(m.Middlewares, cp.Middlewares...) - - rtName := name - if len(eps) > 1 { - rtName = epName + "-" + name - } - rts[rtName] = cp - } else { - router.EntryPoints = append(router.EntryPoints, epName) - - rts[name] = router - } - } + cfg.HTTP.Routers = rts } - cfg.HTTP.Routers = rts - if cfg.TCP == nil || len(cfg.TCP.Models) == 0 { return cfg } tcpRouters := make(map[string]*dynamic.TCPRouter) - for _, rt := range cfg.TCP.Routers { + for name, rt := range cfg.TCP.Routers { router := rt.DeepCopy() if router.RuleSyntax == "" { @@ -208,6 +209,8 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { break } } + + tcpRouters[name] = router } cfg.TCP.Routers = tcpRouters diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index ba5fe4418..b70d261ae 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -656,6 +656,50 @@ func Test_applyModel(t *testing.T) { }, }, }, + { + desc: "with TCP model, two entry points", + input: dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "test": { + EntryPoints: []string{"websecure", "web"}, + }, + "test2": { + EntryPoints: []string{"web"}, + RuleSyntax: "barfoo", + }, + }, + Middlewares: make(map[string]*dynamic.TCPMiddleware), + Services: make(map[string]*dynamic.TCPService), + Models: map[string]*dynamic.TCPModel{ + "websecure@internal": { + DefaultRuleSyntax: "foobar", + }, + }, + }, + }, + expected: dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "test": { + EntryPoints: []string{"websecure", "web"}, + RuleSyntax: "foobar", + }, + "test2": { + EntryPoints: []string{"web"}, + RuleSyntax: "barfoo", + }, + }, + Middlewares: make(map[string]*dynamic.TCPMiddleware), + Services: make(map[string]*dynamic.TCPService), + Models: map[string]*dynamic.TCPModel{ + "websecure@internal": { + DefaultRuleSyntax: "foobar", + }, + }, + }, + }, + }, } for _, test := range testCases { From 5f2c00b4384bc691bde72acfec170d15f46259f0 Mon Sep 17 00:00:00 2001 From: BreadInvasion <70557549+BreadInvasion@users.noreply.github.com> Date: Wed, 15 May 2024 04:20:04 -0400 Subject: [PATCH 13/26] Fixed typo in PathRegexp explanation --- docs/content/routing/routers/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 4ba434771..117dc5b2a 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -368,7 +368,7 @@ Path are always starting with a `/`, except for `PathRegexp`. [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity): ```yaml - HostRegexp(`(?i)^/products`) + PathRegexp(`(?i)^/products`) ``` #### Query and QueryRegexp From d65de8fe6c673ddb730d08f19f8bbbaee16078f3 Mon Sep 17 00:00:00 2001 From: HalloTschuess Date: Wed, 15 May 2024 10:46:04 +0200 Subject: [PATCH 14/26] Fix rule syntax version for all internal routers --- integration/testdata/rawdata-consul.json | 2 ++ integration/testdata/rawdata-etcd.json | 2 ++ integration/testdata/rawdata-gateway.json | 2 ++ .../rawdata-ingress-label-selector.json | 2 ++ integration/testdata/rawdata-ingress.json | 2 ++ .../rawdata-ingressclass-disabled.json | 2 ++ .../testdata/rawdata-ingressclass.json | 2 ++ integration/testdata/rawdata-redis.json | 2 ++ integration/testdata/rawdata-zk.json | 2 ++ pkg/provider/kubernetes/ingress/kubernetes.go | 7 ++++--- .../kubernetes/ingress/kubernetes_test.go | 21 +++++++++++-------- .../fixtures/api_insecure_with_dashboard.json | 2 ++ .../api_insecure_without_dashboard.json | 1 + .../traefik/fixtures/full_configuration.json | 6 ++++++ .../traefik/fixtures/ping_simple.json | 1 + .../traefik/fixtures/prometheus_simple.json | 1 + .../traefik/fixtures/redirection.json | 3 ++- .../traefik/fixtures/redirection_port.json | 3 ++- .../fixtures/redirection_with_protocol.json | 3 ++- .../traefik/fixtures/rest_insecure.json | 1 + pkg/provider/traefik/internal.go | 8 +++++++ 21 files changed, 60 insertions(+), 15 deletions(-) diff --git a/integration/testdata/rawdata-consul.json b/integration/testdata/rawdata-consul.json index 0888c3af9..84132ed45 100644 --- a/integration/testdata/rawdata-consul.json +++ b/integration/testdata/rawdata-consul.json @@ -53,6 +53,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -69,6 +70,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/integration/testdata/rawdata-etcd.json b/integration/testdata/rawdata-etcd.json index 18ed673e3..ffb03d9ac 100644 --- a/integration/testdata/rawdata-etcd.json +++ b/integration/testdata/rawdata-etcd.json @@ -53,6 +53,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -69,6 +70,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/integration/testdata/rawdata-gateway.json b/integration/testdata/rawdata-gateway.json index c47960257..08bd34802 100644 --- a/integration/testdata/rawdata-gateway.json +++ b/integration/testdata/rawdata-gateway.json @@ -6,6 +6,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -22,6 +23,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/integration/testdata/rawdata-ingress-label-selector.json b/integration/testdata/rawdata-ingress-label-selector.json index 703f6dd95..e0351ce0e 100644 --- a/integration/testdata/rawdata-ingress-label-selector.json +++ b/integration/testdata/rawdata-ingress-label-selector.json @@ -6,6 +6,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -22,6 +23,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index 7cd63e333..febc172d4 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -6,6 +6,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -22,6 +23,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/integration/testdata/rawdata-ingressclass-disabled.json b/integration/testdata/rawdata-ingressclass-disabled.json index 458fc2e1b..bbf4d6141 100644 --- a/integration/testdata/rawdata-ingressclass-disabled.json +++ b/integration/testdata/rawdata-ingressclass-disabled.json @@ -6,6 +6,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -22,6 +23,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/integration/testdata/rawdata-ingressclass.json b/integration/testdata/rawdata-ingressclass.json index 346396d6c..3fe8333e1 100644 --- a/integration/testdata/rawdata-ingressclass.json +++ b/integration/testdata/rawdata-ingressclass.json @@ -6,6 +6,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -22,6 +23,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/integration/testdata/rawdata-redis.json b/integration/testdata/rawdata-redis.json index 182276fcb..4f8e8498a 100644 --- a/integration/testdata/rawdata-redis.json +++ b/integration/testdata/rawdata-redis.json @@ -53,6 +53,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -69,6 +70,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/integration/testdata/rawdata-zk.json b/integration/testdata/rawdata-zk.json index 7381d8c27..b06c6974c 100644 --- a/integration/testdata/rawdata-zk.json +++ b/integration/testdata/rawdata-zk.json @@ -53,6 +53,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806, "status": "enabled", "using": [ @@ -69,6 +70,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805, "status": "enabled", "using": [ diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index f3f4aa23e..9411cac6d 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -277,9 +277,10 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl } rt := &dynamic.Router{ - Rule: "PathPrefix(`/`)", - Priority: math.MinInt32, - Service: "default-backend", + Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", + Priority: math.MinInt32, + Service: "default-backend", } if rtConfig != nil && rtConfig.Router != nil { diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 273a99260..bb2ab2d4b 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -528,9 +528,10 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "default-router": { - Rule: "PathPrefix(`/`)", - Service: "default-backend", - Priority: math.MinInt32, + Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", + Service: "default-backend", + Priority: math.MinInt32, }, }, Services: map[string]*dynamic.Service{ @@ -993,9 +994,10 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "default-router": { - Rule: "PathPrefix(`/`)", - Service: "default-backend", - Priority: math.MinInt32, + Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", + Service: "default-backend", + Priority: math.MinInt32, }, }, Services: map[string]*dynamic.Service{ @@ -1469,9 +1471,10 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Middlewares: map[string]*dynamic.Middleware{}, Routers: map[string]*dynamic.Router{ "default-router": { - Rule: "PathPrefix(`/`)", - Priority: math.MinInt32, - Service: "default-backend", + Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", + Priority: math.MinInt32, + Service: "default-backend", }, }, Services: map[string]*dynamic.Service{ diff --git a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json index 38a75d6b3..c46338992 100644 --- a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json +++ b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json @@ -7,6 +7,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806 }, "dashboard": { @@ -19,6 +20,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805 } }, diff --git a/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json b/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json index 9a98c0a1d..6b5928608 100644 --- a/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json +++ b/pkg/provider/traefik/fixtures/api_insecure_without_dashboard.json @@ -7,6 +7,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806 } }, diff --git a/pkg/provider/traefik/fixtures/full_configuration.json b/pkg/provider/traefik/fixtures/full_configuration.json index ed0ebfba6..24597958d 100644 --- a/pkg/provider/traefik/fixtures/full_configuration.json +++ b/pkg/provider/traefik/fixtures/full_configuration.json @@ -7,6 +7,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/api`)", + "ruleSyntax": "v3", "priority": 9223372036854775806 }, "dashboard": { @@ -19,6 +20,7 @@ ], "service": "dashboard@internal", "rule": "PathPrefix(`/`)", + "ruleSyntax": "v3", "priority": 9223372036854775805 }, "debug": { @@ -27,6 +29,7 @@ ], "service": "api@internal", "rule": "PathPrefix(`/debug`)", + "ruleSyntax": "v3", "priority": 9223372036854775806 }, "ping": { @@ -35,6 +38,7 @@ ], "service": "ping@internal", "rule": "PathPrefix(`/ping`)", + "ruleSyntax": "v3", "priority": 9223372036854775807 }, "prometheus": { @@ -43,6 +47,7 @@ ], "service": "prometheus@internal", "rule": "PathPrefix(`/metrics`)", + "ruleSyntax": "v3", "priority": 9223372036854775807 }, "rest": { @@ -51,6 +56,7 @@ ], "service": "rest@internal", "rule": "PathPrefix(`/api/providers`)", + "ruleSyntax": "v3", "priority": 9223372036854775807 } }, diff --git a/pkg/provider/traefik/fixtures/ping_simple.json b/pkg/provider/traefik/fixtures/ping_simple.json index 6f159dce5..9e74ee0e3 100644 --- a/pkg/provider/traefik/fixtures/ping_simple.json +++ b/pkg/provider/traefik/fixtures/ping_simple.json @@ -7,6 +7,7 @@ ], "service": "ping@internal", "rule": "PathPrefix(`/ping`)", + "ruleSyntax": "v3", "priority": 9223372036854775807 } }, diff --git a/pkg/provider/traefik/fixtures/prometheus_simple.json b/pkg/provider/traefik/fixtures/prometheus_simple.json index b636b4569..b385f6c9d 100644 --- a/pkg/provider/traefik/fixtures/prometheus_simple.json +++ b/pkg/provider/traefik/fixtures/prometheus_simple.json @@ -7,6 +7,7 @@ ], "service": "prometheus@internal", "rule": "PathPrefix(`/metrics`)", + "ruleSyntax": "v3", "priority": 9223372036854775807 } }, diff --git a/pkg/provider/traefik/fixtures/redirection.json b/pkg/provider/traefik/fixtures/redirection.json index 73ae77db3..a62480281 100644 --- a/pkg/provider/traefik/fixtures/redirection.json +++ b/pkg/provider/traefik/fixtures/redirection.json @@ -9,7 +9,8 @@ "redirect-web-to-websecure" ], "service": "noop@internal", - "rule": "HostRegexp(`^.+$`)" + "rule": "HostRegexp(`^.+$`)", + "ruleSyntax": "v3" } }, "services": { diff --git a/pkg/provider/traefik/fixtures/redirection_port.json b/pkg/provider/traefik/fixtures/redirection_port.json index a9e75438a..05ef94a7e 100644 --- a/pkg/provider/traefik/fixtures/redirection_port.json +++ b/pkg/provider/traefik/fixtures/redirection_port.json @@ -9,7 +9,8 @@ "redirect-web-to-443" ], "service": "noop@internal", - "rule": "HostRegexp(`^.+$`)" + "rule": "HostRegexp(`^.+$`)", + "ruleSyntax": "v3" } }, "services": { diff --git a/pkg/provider/traefik/fixtures/redirection_with_protocol.json b/pkg/provider/traefik/fixtures/redirection_with_protocol.json index 73ae77db3..a62480281 100644 --- a/pkg/provider/traefik/fixtures/redirection_with_protocol.json +++ b/pkg/provider/traefik/fixtures/redirection_with_protocol.json @@ -9,7 +9,8 @@ "redirect-web-to-websecure" ], "service": "noop@internal", - "rule": "HostRegexp(`^.+$`)" + "rule": "HostRegexp(`^.+$`)", + "ruleSyntax": "v3" } }, "services": { diff --git a/pkg/provider/traefik/fixtures/rest_insecure.json b/pkg/provider/traefik/fixtures/rest_insecure.json index e11a84e77..a9e13e02d 100644 --- a/pkg/provider/traefik/fixtures/rest_insecure.json +++ b/pkg/provider/traefik/fixtures/rest_insecure.json @@ -7,6 +7,7 @@ ], "service": "rest@internal", "rule": "PathPrefix(`/api/providers`)", + "ruleSyntax": "v3", "priority": 9223372036854775807 } }, diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 7a78703d0..1f1b09a52 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -106,6 +106,7 @@ func (i *Provider) acme(cfg *dynamic.Configuration) { if len(eps) > 0 { rt := &dynamic.Router{ Rule: "PathPrefix(`/.well-known/acme-challenge/`)", + RuleSyntax: "v3", EntryPoints: eps, Service: "acme-http@internal", Priority: math.MaxInt, @@ -141,6 +142,7 @@ func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration) rt := &dynamic.Router{ Rule: "HostRegexp(`^.+$`)", + RuleSyntax: "v3", EntryPoints: []string{name}, Middlewares: []string{mdName}, Service: "noop@internal", @@ -241,6 +243,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) { Service: "api@internal", Priority: math.MaxInt - 1, Rule: "PathPrefix(`/api`)", + RuleSyntax: "v3", } if i.staticCfg.API.Dashboard { @@ -249,6 +252,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) { Service: "dashboard@internal", Priority: math.MaxInt - 2, Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", Middlewares: []string{"dashboard_redirect@internal", "dashboard_stripprefix@internal"}, } @@ -270,6 +274,7 @@ func (i *Provider) apiConfiguration(cfg *dynamic.Configuration) { Service: "api@internal", Priority: math.MaxInt - 1, Rule: "PathPrefix(`/debug`)", + RuleSyntax: "v3", } } } @@ -292,6 +297,7 @@ func (i *Provider) pingConfiguration(cfg *dynamic.Configuration) { Service: "ping@internal", Priority: math.MaxInt, Rule: "PathPrefix(`/ping`)", + RuleSyntax: "v3", } } @@ -309,6 +315,7 @@ func (i *Provider) restConfiguration(cfg *dynamic.Configuration) { Service: "rest@internal", Priority: math.MaxInt, Rule: "PathPrefix(`/api/providers`)", + RuleSyntax: "v3", } } @@ -326,6 +333,7 @@ func (i *Provider) prometheusConfiguration(cfg *dynamic.Configuration) { Service: "prometheus@internal", Priority: math.MaxInt, Rule: "PathPrefix(`/metrics`)", + RuleSyntax: "v3", } } From f8e45a0b299e1c15e55e51117a3e121e66264fb5 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 15 May 2024 15:52:04 +0200 Subject: [PATCH 15/26] fix: doc consistency forwardauth --- docs/content/middlewares/http/forwardauth.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md index 25b58480c..97e6a8fbe 100644 --- a/docs/content/middlewares/http/forwardauth.md +++ b/docs/content/middlewares/http/forwardauth.md @@ -300,7 +300,7 @@ labels: ``` ```yaml tab="Kubernetes" -apiVersion: traefik.containo.us/v1alpha1 +apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: test-auth @@ -316,13 +316,6 @@ spec: - "traefik.http.middlewares.test-auth.forwardauth.addAuthCookiesToResponse=Session-Cookie,State-Cookie" ``` -```toml tab="File (TOML)" -[http.middlewares] - [http.middlewares.test-auth.forwardAuth] - address = "https://example.com/auth" - addAuthCookiesToResponse = ["Session-Cookie", "State-Cookie"] -``` - ```yaml tab="File (YAML)" http: middlewares: @@ -334,6 +327,13 @@ http: - "State-Cookie" ``` +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-auth.forwardAuth] + address = "https://example.com/auth" + addAuthCookiesToResponse = ["Session-Cookie", "State-Cookie"] +``` + ### `tls` _Optional_ From 8b558646fc6ffa84471fc8ce7e87842f73f4bdaf Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 15 May 2024 16:26:04 +0200 Subject: [PATCH 16/26] fix: remove providers not more support in documentation --- .../assets/img/middleware/ipwhitelist.png | Bin 0 -> 59469 bytes docs/content/middlewares/http/ipallowlist.md | 40 ------------------ 2 files changed, 40 deletions(-) create mode 100644 docs/content/assets/img/middleware/ipwhitelist.png diff --git a/docs/content/assets/img/middleware/ipwhitelist.png b/docs/content/assets/img/middleware/ipwhitelist.png new file mode 100644 index 0000000000000000000000000000000000000000..8c6b0c97a5a68d4818584110ccd8f38a19f1ba44 GIT binary patch literal 59469 zcmYhj1z43^^FI85pp=vdNH<8gG)R|#NJt3M-ALy_%1t-YQqs~5(hXA5AT2H3@U87R z@Av!f>r!#M_lj9F_uO-@c??xil6j0qga$#-V>ww#RR}_Lh9HD&6h!b2>SfYT_#gJN zT22teN)7*o;Fc@u0znjzoa9S2x76JQZ)crNZ~6-b1v=W-?dY$X)niZ*9Z7f`QJ&2! zu6EMeKb+1xRYbLw=x$8LY;&qvOHLoeo~1z^GsbG;O2JFAVEV?!h3=y2znAwaBA4$Y(}f2{EeYub-0-p4*L1?&Ici{YJu)p%cUQF}@-N~Yyr@fiYx zj!sMYlfJHtW~s#?3w)!cn-na$#hin8vWkafl>QB*r`J1?l{?W>q^7mYVz-A4X`V0c zr;mSQ{clJJ@++(`L5lMF)=!()dWW_Nb?R;+! z86zQ=z4=Dp5hrUTzu)DZl)B6m0-QNJC|eWk(M_~{4=04H^Brn~T)D>Ubl%ifraNU9 zpZQr&8C)fFHm8JMD_9)NjvZ}RuLbYys@6_*?4IH=k*$fM|}ULgGv&rn3cHR#{6TumcFSh@>WX^E5F%F!P-mg2aed$a{rzuj+_g7 z$?fUq22#(+KBdXMdC$x{zJ_lJm>*$Zay|u;*AlMB5s&9~0_zW0aT)&9sx=z9Td)i= z;N`XKWw}xIuR1t3TW34c&>+LtT&h=Ae_Kgz#(fpA)jd1Hm;KWBqlqI}V?Rk`U-d;X z!vC$%ATR{tRn|BVmXm=#2%hoozaB>imN2_t+~kX#)%W!}S9)e=V&eA%At%kWpK4e=OzS6H`H%pSqKKP+=UUdysF5&Oal6KW*Lv?Z*q;)i0qnyeqgtgG?-I8y@ z4=&-KR}Z=W@M6#PoexsfJF&V0n&a1>s7MgNS~BOo<7yG7wP#WM3@j}PO$|=>K0n^L z>)t-P3QIzPp2~EU-!4-n$H#e$`1a=TS{&~+-(Bo^JL{t#WDOo&J#h+WEg4!9J^ek} zOyBnb=|l?28JSXqLVn)s#&PAYWc@Vx{9tI1esRN&Ia%RM?AB?m;i2LZ3VcoL|Foog z94D%6V{GChhWPol{LN91+9le@rfFOA$!wU0VvLRZ7%WfJM_l;Z5!ZnqvpPgi2(^f6kJz>mc- zR+d$-@wj{N`pGfVd~B)Awtmzk*{r53gahFrghCpkZ}l1ksp$}-4g@|v4CZIBu1oUz z=3#Q#KAAg}@aD;CIgx9@#HW$3@odWyNHv-#`|bU=x$Bcw=b}w1b-q>x9~j4Txl>~? z>js~J=`sy}G)tp4{1cU=tebbAJo)jTYfG$NORS$c8x{o%<#zYBL)|h?HTItnIbM0D z_fB08J#aoSr8<4|JY>x{s$w=E^!gbVd44eN%HNnl=zCN&KKkOHgK)?GPsY{ZLlq;I zM0y??cZM4>5AaBmJ2@Nk9ybn^X0X}mZY*lcKGf+f6uU+r!WZ}(g!)ABnAd&ZpHfwdGinXpAZZ$C~>)jsZ z>zZ%;p#$0a0w+SOw-N+nK>9~*mVfdN#$J&VBTVc~XD4`X^tQx)Hq(G~^8QXDRl9h;h|c2g)C_MVrfA_#N(Rl5Q!U zG~|ewD@E%>^pSh7^zQKP6uU>lr}Z1cb`ZhQ*7G#8u=zZGN8=mbsP;jSQwfbh=oR|q zZRjx5WM*F8hR5z0ILOXhofjGs7OT=8{rG>qPsK*HeC2oUdCQ1PiD(vgL451cJeQ3d zi`ZoYS?dXGy?pMxlx68D&oScVYn5sozqsX(+s5%5{*NgCE|d13gUdy2K82RrslDm( ziq_SM=xLDReA12mZFm2iyVA*UBqU2pWA}3L0hbJv`=dg=!XB8Wf&I2b(lIm@ z@nRxtNyPon(?wM%K9nZnWQ}$AhIQ@WvFU075#6P|`kFdcOr}E3`oCfQ>I?9U%pAof z_tot&to&@dF=8oGGk!)-gyZL87TUlS&Sp=-Cz(J#)qPtZV?oSef)2K)mXuq)g+9JS zb1Q|-B>ogwF|I+tB{0n(E&@`0rNaA(1mRoaGX6cM3~GDVTEOIs%~o%VLy}9y%ZWC& z?FcXbT097){+fb=_c&w6Wp_c-)$bf%r_tM0`d9gy5zHWIz0qK}>&{EYgr&zgKccU? zE{OMv*s=d|S$~S)Yt*UU$g{!R^QwON66>RCPU$rp3a_T$2rZa< zeINRuwN&uNU650~v-Gol7&W|wtGzCX!0S38l^h+_mxY3x{vn{Hu(Mp}Eq zW-SRu6>mikmX~z=Ajxk=RaN6(iNC^#!i{3d@;Bs9|ebw}mfXB!1elzFR3yw(?2|x25M2jXX zH-+=cLnKqHiM^eBfWiSTReLaG%|HHfIci&ux297q8hl!PA2=!7;0--40%1z)r%QCG zzxVVaH?UKU@9as#*Tb*H>$yh4G0xI`9J)>Hw?jn(DT!~>RX+Rd)rGr?LUF-eldeAH zLelwRA%tLdG3{OT(6!Buvcfx3Ln+8$v=N<&d`Pw-DrtqeF-BK@USF{hkrM|!BdI_O zDN0KRbAnuyZhR#Xln-L@?_0ac3w)RLy{)mAmJ#~ZPWHtwhL!VdGmNPHvMUnwMenMG zTBkR0Su6Ss6|!f3+|VxGLMlF`^B#Ky2a&OkTY!MGk?nhJB_a3VfV~|>gA8?-wnUE% z1K8GjwR<4K1%}KQtGE zE38u1dsjRH1hp7o?|Zk^vYChiHnO`L1sRqPg6A7GFs)#ib%nXt*-2kO*`NJKew@<2 zExTn?UP{M2+%Lht{N|ZUUPbEdDd(+xQYO7xQRwktkAl{Jd)&YY_M9vAi=V9HTB~;+ z;rsTR4(n!4BINL6%-veq&Ct`nL5Pv^`y?p?RWt--2;NoCH{_TSuk_z&z2)%*c(G&{_(HsYni)!x%Ws-=I2#&H zBxqpJI)xk0xE9pBI;9j_Vb@}HdgQDA$Pl@ zHbzVy;1pUZEyoXm2`v8<-*nxduIX2Ce|{F^Zq?>_msVzWZK7wrzA`p@qThPkx@v@+ zS!kWWzW9fRyp%k?Iw|9xj#WCS(2z5^yQim*Oj${3+qoh2T(4dIQT%mxM_aR6uMSjAt~7JnagV^4|$iJjJ+-F05flhcxy z9)J^GP7Lmpfxi5O* zJu}r959aG*j~8YCv(N7=B$dG5S~~2mfDVWy%Zt3dU5xi_^>VFoNZhVfy5T6rLHbHo zF9m&>{i2t2XL_{UP+T(#aPvS3`f3a;c~l^wZ%kZ# zavZl5O`aU{g&IHn+_tnXVQH;i8@`v*zA=-$G31!pn%4m@S|bxd87KD4-RW*_z}4)9 znWE^vSOtoTx)PaJ*TwFZHML&}6G6biC8Cq>F#BK;{oT!v#ZC9~2V(0+YQc9D<;h1u$i}`FL_I*zh{r_H{iVnpZ#DwSo*pKKzEs;v*FP1fkaC zu`&YkTyo2CEG$~I108{J?$00g6s%|A-CjqOsG7*RSi!`FS|Gdqo*XhRcSuA7e$C^# z^>zd-zsVudvhuV0CE|aJ0Qq#P%o`-h1ZghdY=s}p4~|Jk?l&9q&zQ@GLPDV$6aqPk ziSBoWi4muN_L2M;9lbSeK0ybYMbGZ^BteMDW%|GO? zQpcBp@=9jOkZ?mY_8oXPsS{#Dw9$s=FCCrBy%&>+Gf_xJunI6g9`tg&zN=ui0 zUQ{0w5!8~*xf#{X6#jSAGl5o1H1yQYGA(`th>(4%&;?ow+sct7FtaLG2Fj46Z$JBB zE8o!^A0PCcliSzg-gZ*laC`eGP%%4(L-(eyDKUUbf3txaOpIq#N#`;e}h-0dNK8WV7;*tKp4oMVVVd~PS*x(+|}pEbXy3n zjl_l2mind`5LWMw|JFr;NbXfnCnw78DwBz#ajKvA-pZXu|8-+nY3 z_B|dQS~qudOIp|#f{oDVK8+J0vHhw93W3DMxzt~`ZyvRV?BF1XeFt#>@bJaRdser^ zpLtwt@Z!6xvWoXro|5n7j0lDxWKS}CeS@0&Ji6&=)1~)fnj*05nCFtj3>Abzv4Fdn zDw^Vsx}FHYrdbu~b&HF-i1 znWx6xm*N_St6^o_J}C_JxW?t>Wq@lg;)yZGm~wT6oa>JDmZ~81n0(9}>aqmNOK<-}rZenZR9t<4`k|GyIw)@8V zAQKWaJux=j#y-g7EX_je?d{FO!&6dHl4?^ix5rbzy}ix&qYx!=K#DgdyR2+v-Lbsy z)yvT|mSbj-0ZVm}jL3zpprD7~-xJ%``_>%~u@VN>4e3#sk&be1vaqF(dVl`Zhz|Dj z@@mS;q8w*pWLmbbtFGqRR5!Wqe@0I3TCYnN8+!i}`_j^^QinsAmB0?!0x6)aO2?#j zPO8*@u?CDDUB+>Cnz5BjwKsg_C6YLhJ8C}WGx4#*V3C^8LR5%J?oYN7{a+)PSl>Ao z-u5BuW&wx%_6`tI&3!hzp9DOg?Xe*-!sn7r4PO40YD3-K4{r~qmTosjmdM`>h}@=( z+{?5*MTFP^ZKT#@zTV-vKZ{-JHpf%rCfeKCIllC=toCqppyH+fE1%qc?~&I0Z)6zE7Wt+-P<-6s_ zKKw%X4sFlnI)Nw8a53OyGct*Q-Aw;t?)fs@kc|s1>oSk;*Xj4@^=L0EeQ&~n=-O?j zoFQ}}zrIHb;m!JHxZF|+oUJU65Uc68zXzy-x70P0r2yLmn#>T6M@;66yevCpY(Bbi6Ka${|m8eLBfj1&}yH(mak_TcUW!~*iqWc=rJMhDNx$nXjA@o$-L%xH2=!KVicf16ws z;nLmN%VfWf?W5V);}Q}o9yRA=N#4UBvwIpq7pYu!Et;F{t!ycg9P5jjk|g;ef!OvG8-t zSt&oZG(OH=U5ip(Kbfq}r6vb`wlh1sPRR3%X*J{WE)7w*%ZCrByFBCtC%AenOoSMp z6`x&a{ODk~Tkk>5sF{zc>NBYcH{c%;7x1e1P~PtTHocSF*27Yo!;>cOizZ=#9| zKo#CQbB6AORk?%Aq}qDzl01R|GhbvE-nrpY5Xg(sdsm%I4&w*+D3z6vn23uoOY9^s z9eL@p45(+t4ryax1FqC5<6n51J}y6kIAr-(yr?AE8e{Js@xP%cvQgU?`9z5faYXc1 z4|=8~(s>z*b{9F%SOfR(>*IA6NEl*)ge(;k{JkAa01+_5}5I?6(ghATZ* z``fu;cvz`HkT!Wd+FLKCBXq0vv&Gt_&cF=KBGvr*WpvEQCVFtkN+gA-Zq7aMo9^yz zqzI|u8Rg+J5+iv#JGSeIQS&%8`buJD0m}SW1HVsE*eLv*CD!K;oCUT@sOCE4HhQm_ z_F(?L47E8LMO*Li-wM^HJr3?j-P0_ZY<_P1qGS2n$fm-z01euCsAdaDEL+pcaNe_K z1af&xJ{JA{66WHGHz<_Z6X0o*o4ipn{qd8p5By|-)<)LqV@m0u<$7_vOPvn9MPq3p z3~gK4Ko&tP2hgI#NnX~HcO8E5i2g}5SN(Jaq_Uhb9Ns{TgwY7=1bo?SB!^%9} zZve4QmA)YfcQzYPY}ELuzUAC7I{G?h-M%3!A~G`eqvEfN7&MkE?3^^3kp%InjCx&G z0p)(0y2g@C=GjO)GA^u<(Ke;V90aWLBd0KvMXCBHe4xP+~ z7fKah$G0xnz76B03`x^@nb^^lw zpw8WACBDq60EPpgz@CE&T3gj8x!h~@JU<7(05YA{0%aI2aI?{Qc^+PnzY&8-Aj#S0 zH$UqXc*uRfI39-9d_^y^v-7Fg6$}M#b{H{{p)IRu@Lk^j&(@veyj!lST*iztvFM-c z%p9?-~LwolSWr4M@| zJvNz)7;nr^!INg8FO;n3rW5lgUV{3A?;hqH zWPIOu(AD2#$JR#~wP+&#C_y?qdZMqqHY8N2>q$~2SFOc8jn}&I7d1xoa_zh6BdgO3 zS$A^JO0_M!tg_(DDYi=|b+dItP!n&3ewxvOD12GMNejqvM7!StfdOy4efw#U!P&v{ z-h)@rS2duvr7Aoi4~CUDzUU(lJ`;%~fS?fixetWPhFK1Dn?(}~idF+$7pd40Z+00J z+gB<{G-y?7a@81ny1V%VSB@_^X}z4w^OAV;PclPM3FN4UsuY4cmH~x0octom9(YTS zTKd~zpemn#fZL ziwOd&b!b`>DIC`BCLU+Z2(^Y{GJsH&s*2kVZP3!Mk_m8l1Rb8`f5I(eZ5_d>vkd(?#*MVG=-T&Tev*4lahBd|lOhxSKO z!=0a)1=YD>9zH6xU0U>wp6B8 zQqa=R1?c>m1Ms*;a_dFs>wSzMJU|~?B7k^Nt|^Kj+rI8tT~kvL9iUwTA{@Bd@Zcc0 zu^t>8%y~>P{$)H`h+ccZS3NoAX=G2w%udz1>q!S!IBs}s>nCN~T^G+GnTCc2KH^np z0Vz70WXAj7qfbK0I3E){N@j~%oFSR5J(9cVr_^QDl#qDPs2vl1oM6Z&!$^WTW&Cq? z_Vd{{$Cb(An%2t7*dfD!w#@IcX!EpscY;M4Mf}h9E-vc&jK|EmSreJ|B*do2W_jRo zhD-INZsl*58fDIJ8G8V&`8iqqTD3nsTNqMK0C&A!S1-&!z3{gefTIwKVma!oXo@&S z>|#%iISZ{QX=6-7CEzc3YEV136=FCbUTQtskz{tLbytLoVk9jivqhca(ja(+AISiSaDOwnOAYdih=AFo70ApbRw|BV0aD2uf{5W+8-*N&6)^1R-M{% zHZn3wj!?=w7tv=YR7@Fm^k6n?ygq4LX~IOFz1g6W zExrajNFuUZUv~p1w3Kc@y#q1es_G*_Q2`C4*T4S;+Tg?3?V7MyWGIT~s1LWbHYAG) z`%1BSV10XMXElD`dL@T0KIes0JL%D-m$lG)ZlW%=Ym=Dc!jJT+Mu$aZA0-k=#yI`W z2uh!s5D*d*ugdTx|L}euE=}FsCXmKD5_-$GOr=GipJ67#B7E$-9tQGA;XGjEHCT)W zl{0ko^jx&C8M1j60(`eGioZWNBJxI;ZO*4NWdx=vZLX~u=V4)k(xBA(_s&kNaV67R zs>|Q4M@ebOP>#5W@=9L{;bve5==tmikSDFMd3ypWoxXv6PB(O38pt9-~byYh(-FCF$}2qpGl z6pE#z;~0p>Go=Ex7zqj2?5BF_vwNW2NVY`iQjDbEcxJ;(+&t@Lm@|+0qB(kXb@gV3 zJY0HpZH8mkxOL z47pPjjR2nSWu@w`&4HluRkv6iaEj)-9uUj-*9xWioxYzzc^-U41@%t0G(0HS2#joC zIxyCZ^{Q)`*)TtFBGiU@HCD+!@Ezg@J9GqZtI&G<aUdYH5pZ3T%r~^wl1|-)+O1z?L9zXQlu!`Ry4TpD~rc-WA-(ou@KlkzSP#y$wgD?`sIu?bTV;L z%9;d$&$B|Die8A!kyX7!XqLaTg+$n&b&4TA3_i&JftxByp_oVVWE*5DhQC@cKM$F zN(~`7xf%82ck;tJ(VK-8Xas@OVpjz-8ySedRq~TeoKn}iFLC^Bdd>?FLtLo2qaket0WuZ?Z5r${7nV2zUnI;h@ z&lEY7JD1Kaqi)#M>+338(9TJLTL8+>-)CrMwwWgA$|pSCw?`IfW^EmI=ka4|sfQh-~k2W7GxR5r(n|kUaKnBavv~v7{N9`*3n1ShXdLG1bieO29UG=Lfp% z)0j{kjE6J7-V~_Kig5DscGv5sy?b^WbyF~D=`l=}I6&*OuG;K)VfKSCXlr|WMZZ^{ z;-uTm5@B|3ZeT{n6OFj+$kqQ?yT8g5pB|ZQV<#%ohYP>*Q$v%r^@MdxXdIjRwHPFZ zKtrDCSbpTtkR^~d40yJG5iCradLfaPDEtdtKLP@h&Yih*0R8Njea|a)%*P-njoUw? zq5o6VXo$D@QG~SVd3=8rg50P^y!two8G;gSwi@FWgDf8xs8?|5inX$K){qLH^1GT8 zmd>fpzK9qyH8RpZe6kCYrvYL2>fsLr#^t}jX{WU{Dx;g}H4flT&-5h5LZ3G^N^tx$ z>Xj6OZjuht$u;fXz(c~I0s~&cAg!0JPoSuY*IMWsy2THHh;b6=Z2Tpd<-8Fj+`F+Z zE%=Q$gr|SBOyoXH-`+)RCE8IA`H|QYu?DPtQc_uvGnc90q{! zEEI&+u^bMK)>G22D3OqG=(|~(pz*2K?@4~6)dN-Ht`IHQwue!r08phZ&q@IpP2M=2 z_tU1{HPv=a%*L3$KDpsvk&%%El=|F6;U~vqry<-xHsR$>IAiBe8?Is_PE(Ru+IOty zByF_kQ|xI#lykc4Br)_zw#F*`BUl^5T&y?KB&k{NC>*>Unb8Kwv4d)Qm8Z33y zmNR*b&OZ4_%6~L_iX>bb*yy4x*Pd^1rUx;81aMi9JY)MNe*BZm2|ifb z^!>!x(ufUj%AXg~p8@YGJP-iX+2Q;p;wlp4?t#Y|^9+-H6O>IxlpKMFOxTAwYERKX z4XR5sE*A`7o;Po7%lQ&dL9V)x#ccfK(u*~*C@af8J_JIY1>sylIf{3F{Fx6OnLE>ME1Z9MZ7PXcr7#`gD|)0L5Tc(h4NTz;xeoP-&9g zn{!)oCd1ig7TswX(4cPYr;Issz3Y#rNH8Ai=;&;Viwf6ii$w3*>i@VtFryuH~oLGl8hb!kMe9vl2{_Jk0l_%M3sOJrLDWt%1N8lwVXd zU|Ty50}i8Do~m84VQdPPZ{0Bsj4rS`y5+n=tSa}9h^o$p6QJNFD?H@OM{%bYu;T7z{+c#)Wj3$24JU z3pXKvh~#X*6*)`(GP9$YjS}i`FPDU;7!UVc1REKZ{>b5;D;}^>7&o%ZfNM76;Nz!i zzlgQ9Q}4Mqg@|835OaxgEQ%PQ4wG09p*3_HAaeKn_4Z5oH*(^jnASL`Yb|G_|5_4% z7%W_t1qzMlN-_58$&No1Q#^zSq>F$#0DPesT!HR@*WMHjY&oN0 zSfmT4*r=KVzDU16cN+Q#RIYlU_@LYTf$-09ln@gENCQ?@O!YyLu4eeVE`kU0^yQ8| zmX2*2;N64tA%*cyJF(fBC!Oyza;vM=Z~Q@?u`@Tvu3XKrLcfox0O{Gkuc>zZEpUCP zT?+E!jOs{*&EPK?dm@)Zhea`Ka+fxYNF4gW8X`IP@i5yg<#dTXgTCf?HkS3q?LtquhmF z#gDZKidS_eEOeoi!ek?W%Qd$v3W_TsDk&+U*%kh+?VtL{p_lo$ir=w-vD=l~s04c( z`4cw*s6hDi|MX``K%3Wk>C0aR)~T$1DK@0W10I-q%zreIT0@dE6(XIDb5XMCSl|2b zQNYV#9=)6w4-Rmitske0t^XYfIq?tYp|nt$mf5-u&#Q1hqt5(@~T8&BZT)JP)) zhXRU+TjgQ)I8Y`A;hWwv2Y(vBS9G0IJ5j;1W0YLM>L2o=T9MDJ6v!fjciH4#j_nJ2 z?^FD}`m-GPR#;g%4&=nVkk%h_=G>rea}y#P-k7njDKF>n5mgj9t8;d7F|CfY7JO5& zZgBuG!%&qQh&Xl+JsY)4OqVcI>tMrHU6S&^w!7wp;oVhO(QEdVq~AR}J;z1RIRpgc zU0BAuL)hwQ9>iPFGRJ&dtas|!a(2{vFP{rWi-?E-lYV<30yOc6Krv4}mqdDc`tj~y zpl}LpC9pk^a{+QEI7K(1^r|`Q%9&oo88Tf|QK5l>yQ@|8>Q+Gi>6KLC=A`&fXkXo| zh2oW3aDP?|CpSOgM3hVaCbY`%0quGOWE@=lpbi6no^iq9XCJLyC4b>^9CnI6Z#JHl z0i*{M&Uex4#AIX}6GUiXNrb6J@WuwiD6#=T4h@JRYb`*XGnh$MNl`9*)O>z=`qu(; zXEN0{u`Es67Vj?*h$Vjh>>2ah12oh63LENzk5Zghq6ViS*zES z(TbO7EY&*&yK+ewCMmj_#B8lo{&D%eyE|w(u=O!Txv--|jZtd9@p~x_KgMc8oO~{e z$pCGo<<|jByJ2!p+tAMFUS3WjP-e=M(M`5CCahHJzW7hz*xVc_j*(k#(XwCg)TSnpv(E0F70Y9J zi-25elJRcV2tkF`wzw%=;xTk7QHKJldf2VYR~~7y=criz&S-PbL&q)s^bQr30OX&K zeN?jIM;-9SB4`FT#FsD7N@oLxScE%bTKPO)UP(!s&Wb%*F^5mJL}O%d5bs`vIJ|pa zpxI0z#-4)&(79&9J-7o>f0?OiR`cK0BFHRM zNs+-3eLtR6b3PTtg-K_FqU9#JKD6pZLZIrLqg+k!@cqa93f?>vX80-s&x&uPuAZg0 z`@!i0#Hk8y77w(KD(D-GzR{!x-BK>{Xr_54#LD=y=|g@@$!9W^FsRB3ynX0+}O9Y%jtCgBmEuxiOSrPky?imxkh9OVf>cc4FJI#gl2w>fqbYML9XoUu;kY+@OG zZa0i-lJ{uaSs=2f65y|Z0Qiv~hm+&)1r$3qe9fU^ARPaDA-AD^s(2jyk%_><*?#@d zZQ55l`?Y^XYky_*vv=dkk&4fj6KcHPgQFfYO-W6qc}bmbWXVlb664Md{6(G|7~{Ve z0}5E8jQkAm^IRqZAl>I<(zOKjc-FJ2rr5^2L~~{ ze`Jq-z2K*Vd!IcvE3ok2m)J{t+_vKOEN}^}Dd6ZR@AHnjge$PwU%!dr&-BeYek%|q zX02LZ5oW1#< z=)cp9;Bg<*c>9T;;e<08)0qyLX!1Qf5LbX_1I-laq2!<;l_qezZ+phok7iz7d}Z10 zei-pD0sx2CI$CB7Y9jhUNsg)pRD*Hp=is6@f(#t-X1Nwe9-g}&EfxXtKBGRkWY5ZK zGv?&s=0?G*1b4j+Yut0$$rja@fT37pgZKi|jg>0?H=T+lytaU3*|1UgRBwEInemMX zyARHb)Cxc21&SM#zom*sktTyO!{%FF1au%nf!~^fA`=TUfh1e+Pd(O5ORSQx@fPu2qR$4%WpH*;FMCIPVc#lJ*mjc z3o;HrX(o%5F;1evW(Bv5cgGrxF6>-=ERA1+UESRHIvQS_g1S(_vAv5$!f*^TaUl?_ zc$hnxH|16S_#v65^^HcUg_V65Go7P4aD&{9P%`-B zYdn7jJ36GxFk065{rgPNSW!?A+G1sIk9`RY4iMn;?0Tp@&I|-D*ucXatD4o~SyA%2pOcbg1tE{-#btXWM1NTt$HSGbni}4-(zdpj zCo;f9@TY%({Nn7a#`h8wN^tJmM`bfW6DsOp^b-Bo$8*;4n48q+d+%;oHkYUw$7JEx3zpFjgAtZT)kv0B@P0-MkCtXP+o9{p{J#$o_` zh2Yr@z#8C$bM^2K`e>@s=JfRR+FDN1_pl-KE+jRO=d8biTqI?xtws4VS}A|^K$ET; z?})))Ei^oHOyfGVYyi}G^?P-y1@jf6C8`DBQX0_c1;T3Wq5XyF>9@1t;2j{1u62E1 z;sLZ6ADb|>%8?I$CAx$P26&EQL4Hz~WGR}M_y@rqj#MbB^>cXm;~;~@rn|jb+ndvT zOz%U`v`nZD^|qbg&-731UbNE(vAa!n@0^~U2wmAd>O;SLzWV*U-=w8F177*tLBn#O znBsB}VLYt(zPnp2pIdmAg#m-avZ^S9k`a?H(4|-8a%Y*AyZy0V?sJ?6#IVcx!GA$ zGc!`mVDYT?gwk0m=GEF`wDIXSqkAssA}pYbfqqO36@dSuzD$M}7{gFjC+zRrUM08*?VozMqXSfc#MH%<)xh%pnY+L?!hlrWsdRPaSPk9- zRdd9!JXOT|vy+pK&fNF!VsG9&>0rOTz14d4S@vAg#4fbUun8HS12T_5I|E-o_zAhD zja_jeqM6VX>RnRucnqFqVb6iXP5v3(i+#gCj4@q?&z?P7-ktgW9U{d(>JT?zj4y0# zOwrq$pHH2%l)8lnve?s4^aQV){t~~#iCm#@&xwBr45Nf(zm(tA6BpSTF*8MWEdQd1 z+1S`ve@?O~?p-P4$42>+0N)e*<>ys0-x|n)1Qom=2F#|rJD8ZTuOD01fRGLk?eg{x z1nZscItjy=8l$?Wr=VwmF6;UAZBs5(h)!A_EKXTA1#@ZpcPKzWRsY&G(!E))S+7jB zz_2{2%P>@u>ZZ-Y%4&S)v?;~O;qJ!$s-Rk%B?4KkM3&~L*7v%$tmz01_2Sm&eqsXe z8OD+6pr!U3KXE>AA>aMrjXc- zyBg@ZtqU@G1i8a6_fZDkG21~W7BMJZ!|qzU>^j` zJ#VsL9QeeU%)L996)BF>s z)|_5RDE5+c=3EYg{qJ72=2~UKAD}?>(UYWxW)?*Es zay_R~W5D}~XYr`tnm6{6bZeu?!pt3s!8KZzCQkg0PF9Xeu$=0;1;lf%Dl#I%S$Oi# zi3v5}_`*1=#CA=GGb28{3l-4VTM!{Mf9X%Z+RY+MkioXgeg^`QpBD{z@z1?^7+i5E z1WpzafjNwlG_(;O9!@pr_f>`A#6#Gk4NWDwNv~{fZ-XYp3s_%5LIV75t@FM_W9R(( z@xuppGd7w^DzSFS&pj8jpcn+V+1Wiryj2#W5m4GPU#jy z1a|)_8iDZ8uMjs>5^Q7XAir$qF%lwUJem`Dd{S&n0wAVr0Jm-gJ;B!SfJ(n%IbrJ- zRkFK-PR*=g`R~)K*8A($&4mXl2_U5d>+0yF%IX#Vt9`WWM_rr(Wy-!s^jg*y7Rz`_PoF%mJ-WUX zy;;UeOi19Pjfd8|_FQ6PUM_1oy1Leab6U7Txwc*ZrMsX=XHG&wVoaD)Tuai;}7e8<=)1ZzsE#77Inc=+J{H7O}+(_9izFY!3V zMIv^uX}aN0*o!s2MZ27to0yQy;|Ekvqv(UJ?JzhFEv(+t7Q|PtILmYFOGQ2kJ;y^E zBYW7I4naslQcLhw|Kvnau^vg#!(P35#TYMNHl@RiU!oz%!!x3>UI?Nlku!uwJe%Ypd5~g<5qzWBm4DG8YRAOOq#= zg#x9o?|I`=*N73^ntm%NaEJetuJbNSMUo09hB>B%+M1HOe*WiQ4zlb%E|C@Wf)+Ql zZo#%_@)JK%u2f#_)Xu3z*%KCh@c$9SR|x{~{cMWdEaU7s2_obt4}$f?qghm{Y^t)4 zs)OCh$w^pP*t9Zv@S*Gee7$POAs$*6$eEX(mX=^=+2<&6Y_P7e@o@=NuM!O_cEaBq z9mb|_V)*2%VQ7KvmeoPxSpakf64^3@JrlxlQHv&tP>=v$=_7tj!1DmU2|S{LSKNdc zhi>x+=dmp>;c?+18zU9rCM3ea!NJMN$;aomJ@GtHeA%9FI!NL#-+jxBA20u5d!is6 zuSBB=FhAS#Ky(7Poq#65EZl?^$H$Gz-!jrQz7mnF z&bP`k9LM^m=H@irNkSAPd3qyzK9WQ>eY(CL0RQb+3~w-l13E9)njm@&8kUg0t^wp( zQLw}Ew}}4VyI=$}#DW3>vP(w69WRF+{*VDN{aaeZzF_F#Bfowz?erjV%6NHDs7aoM zhUdvXH3HDxTn79hwdg@8*ccz*^!&Va`CHlB`Cq?&_4cBWD#k1gn^w-))@HlC);6uw zVoi)CQ}Afkt4qLxGCe@2&bR9;ybak0&{sl#vG11Ejy}oUJUnT_o@ev*PQV3j=w277 z<^Cj!PD_h$4tjZqU_y_F_81d0ro`6HZZnnhJ>drMBE7O&zQFoavu6N28oyCBfn|e& z?4uZkJ+ScDLY$3@yw6_66ak=pyreYaagtQgViaPI%r2?{H8nN<{{ClYXW)nE=;-ux zvfLeI24X@jh`}x=@cAXG{nd$ufEJB@*OmoApX(`%fNg9%&aHWIW@ZLB+jkif%;2R1 zEF?tZtAM*ZA6`G>zsy!V9fx2`K7uPfAOLhh;hQl+nf+1+te|NRq$}v@Klh}yxw&w} z6?U7hZ&z6$ad`29?phOp6&`wMmvvD~I}?3c#vQt0+-8CI-g0O^PVC zzkE*?;8y3VL+xBzO(}$Ztre1(#IG?%3OY4UT+gd z=Z_!Q78*z~V8B$Po&@$Us4S3L$-4r6g1*xpxKKqj!l>6NYs+?~iIjW%V%USi$4~bW(lm>~ z?G~f>-k5JavH!IPh6p)=r+Swr9E^@9qs@kh6mz0wM|_w z`yin1ZSQ~VoUS#DIkpXlxd1FSshWXQET=0U{52#oF}WcPOs;4$rtc{bL^X;|J(_c# z7y;UyTu3>d_$%fqtP2PkjDJ*K22%0BQZLm)aBl+TsJa1u)mBxVoScCDv@QYS4G4{3 z3c3~`b_u6ab?X3ODc0c?H-k+1+ua@gdx1+ir4}jR$d&x$76vf|sjmUBwp;TGl0=A6 zV8goL2w1iA=FJ=HOu)t-K6(_*wxO7-YQl(XLTGAe_@%zSKCB_<|6}UCJXsMLmAz*O8Ic_s*%?trHc7INohX!bY;lb2@1^(W^ZEVGA3f^P zIp@C5eZOAU^?W|B=XKpMnqtH56rvpqJ+%9zBkgxiLRfqTEJvc?cG3iRQvnh*K$d$y zO0GwGI@OI6PL2^MDWQ-`hUE%K&k(tFg`M=()z!?*Opng?_V!=D?#s)EHs9AkA^n;r z9}7OP{h9?~!$JwWQ9DBe`ZKhJQtKd4++GcS0IXN49|*qgpzvOvIe$i#au=@He#w~eZX8+4^?nGjZ=*q==GB+(B%0_p#V^l~sR^WYM~SJ(JLG6x z5KqZ)TVA^Cx&ZTc@wib=ODjEwi;wr0?L4Or-ru&ri&D+hKv70)?d;&IAa2=QQ@{Bf zJ|Trq?scF{>YANV9r^Yxq?}j|F5Ik>r}*`?`FZJxA$B2m-y`>?O?o%U!-{?N2;E_j?hE&Eo=de}DfD zH*;h(o`QC=3+6HC6e=)(ZVIf-%?)1BH`;5&r>#L9*^8f(^5W=U5Ai7W+N>LKvoEc# z(a_v@_ej}6Ea}@rk1E|X4yofa7mgeM{>W0}v0U0|I(lK&>u*Ut$8juw{Bz4w@tX)- zhT|Va#%qoHBrQBq0rr1ZOMU$Ryt8jO+IwUnbUaULJkhl0U+8dIX|i$5$2$v@L68NV zz5?R?bA0U*f+4{Gou^g60+@3T!UFhxl<`wCeqR(GS}f1czk;;^G;!Z^s@B8|b7*zu z3KK$yv_kF-qF^XTP12;rLkVst#JQm6Pi{t;a9`sJ`uFztyJ~N+N0_4J8j~KJ6-KfB zLo!Z&W}UN6O_Qc~hg%?%gF?0`n-3(myMAiRi`Pzer;^TK0<*RG_;irqaD`2HM}pKNg?f}P?s6yeF_h$t5(IV%zqDV?_cA3VtBiy(svE9l(O zD$sdL&&hWD_gFP{uZa+2;1O-I{`tkVSn=M@#py({t38erhq|~wzkPn#%LL4b=wB?T zx3{$wbze<2;*y_b)ZvL}W&E?mO&M`U%VS4+6^M#SslA<@r0<@9g#3j}b+$u#!p~Qj zlFy5ih5fNB(E?PM{+{9Td8l5~hM3#gt!^$1=7=MK6eOJc4RVyt4XP{v8gLuef|M#M zD&kw~?L_|UA42%Ctt2{lcos!C@K@kT)o%mz-EgW7sji}H?B@!!ne)v>+U1(oplmPKF`Tyj|t<^rtXw zR7}j3y-ad)^2eQ!&mcS2pbHurd_fZ5irB^P9|DP3Sy@T`Jf>2WBn)2^Cj0lw^w3be zNokupVmkuJ?Kj)X8DDn*sZ8=we-eAJ09hcJ+g6&Cx*Il5JYZo$v}ZW&Xu8pX$K6+b z+JXQvV@F>|vq5?Ov|5HFaK66{dJ1_U8XWrh=Q;OLgs46GgFhSI{o4TmS&flho0*2%}nMfOq&q1$$=!Q`T?9wSx5=IqWy1m3IX7%^xmz_$vSQ3P3ODeqGf*b z#>$+4gm>f-uIKeYca5GJJ9k z5)k}kNOn1MY?KiTTYI2_$GXOxy7CWT{ApmMHz^ewoXyWEx|A#;Mi%yRY8(X7^3nPg zORd=`pZ6BE7MU>Jr`Z2>{tIfri52&rBnaV-JM|l7j)Vc`K{{;VvFc49wyj)iI+nB_ zHBwPkjai@`B~=99P4GYcWv}c^`=#CA@{RRnAn00ISt-8KTptb>*xMoYBfG;Z#~6dh z^_CB!7Pq{-7FmF%Hq%4{&Uz8sF4bVdZGQ!Y{7v&&f&XTLQgDz~v)|!}shPW-rCds< zLam+snj~_CmrsXFk1Ijxgx3lQUdq7q4sLKSiB3KYk7hYfh7LiTIe>5Yx+CVWHYh5Iry>E_rdC(&sgT=^2oofP|@Dt zNNXsEfl6FS8Dj2Gy3oUQToG+be*L;7`8?{X6UGQ7*i*-9Y^I$O^? zejBX3rvjcDapec;mXAg{Yn{1SR<5_s^i|d=#{HT?v|7@FTb_tvwPTQ<^%40QT?BJ+ zuA+&%M(heCBf??hm(d33hd@Qop0dI=aRO5tC=$GU9`N4W+}vQkdURx@ijRf0_4977 z?h-b#uz0LROEAD_AWO~n8Tv^)KN_Itw!HlJ1~bZRDATUS9-FwhyMt%}b1&SOzyO>s z=3W8?0<{p~ACj=7JNx@WG{vcBEnf^{dlsR)a?T&eN`L9gPUF51)lu7V7tLkfuIHx24DZ-8WW#+j0qii0 zWOdGNz}JD&Uo_mQr>cA7Nc|R^L8T@K@|;MGGqC23$FRLp)$?t|u7Jwl!L@(r7uo`~ zV8~H_*Hm9>=nZZo`OPDljL<}0YO=LyHdso^iEMhWscHJ6S06e@6Stb*b6U8D=6KyJ z8Xg-OdbTWkFXw#-Imd>fY@V-w%*P3^jNRw?GtKo4P>SL$G^O%C!1A)yyx^}-3^96_ zMwpKec5(+5C!g_bQE6i*X&0ahY`TQ71!n=lI80W}n__Lc zQM6n45KS2&%Sgp-lH*U&+tU-XOX?AEN`VPUFmi!w(L;+2(^^@?s>VG>~tRBv~3|NF-%*bw5W`)xoI5Y9k7F)Iem@t(_In{R;3ptPc){hpckdpAl#}cV z`Y%>HO;PchFZ7}x9xsny^0~H^%)J*wH+5eu{er5D3YmbK465{M9H)kwH#61Poj32= zo_N-1y!2TJF#M?VpmY3V-LkDu;*+H7;ccACq{}_>4{B#mVLnZNM2|KWe|?gqb??vH zdd{%s?E054b3B;ExJlG}rM|_fgwS9

d$No7{;b{hl3DysLb*ryK8|o)4}D60_~i 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 Date: Thu, 16 May 2024 09:52:06 +0200 Subject: [PATCH 17/26] Fix OTel documentation --- .../content/observability/metrics/overview.md | 406 +++++++++--------- docs/content/observability/overview.md | 4 +- 2 files changed, 206 insertions(+), 204 deletions(-) diff --git a/docs/content/observability/metrics/overview.md b/docs/content/observability/metrics/overview.md index a49c5053c..c4b57a3d2 100644 --- a/docs/content/observability/metrics/overview.md +++ b/docs/content/observability/metrics/overview.md @@ -5,7 +5,7 @@ description: "Traefik Proxy supports these metrics backend systems: Datadog, Inf # Metrics -Traefik supports these metrics backends: +Traefik provides metrics in the [OpenTelemetry](./opentelemetry.md) format as well as the following vendor specific backends: - [Datadog](./datadog.md) - [InfluxDB2](./influxdb2.md) @@ -46,6 +46,13 @@ addInternals = true | Open connections | Gauge | `entrypoint`, `protocol` | The current count of open connections, by entrypoint and protocol. | | TLS certificates not after | Gauge | | The expiration date of certificates. | +```opentelemetry tab="OpenTelemetry" +traefik_config_reloads_total +traefik_config_last_reload_success +traefik_open_connections +traefik_tls_certs_not_after +``` + ```prom tab="Prometheus" traefik_config_reloads_total traefik_config_last_reload_success @@ -75,13 +82,6 @@ traefik.tls.certs.notAfterTimestamp {prefix}.tls.certs.notAfterTimestamp ``` -```opentelemetry tab="OpenTelemetry" -traefik_config_reloads_total -traefik_config_last_reload_success -traefik_open_connections -traefik_tls_certs_not_after -``` - ### Labels Here is a comprehensive list of labels that are provided by the global metrics: @@ -91,201 +91,9 @@ Here is a comprehensive list of labels that are provided by the global metrics: | `entrypoint` | Entrypoint that handled the connection | "example_entrypoint" | | `protocol` | Connection protocol | "TCP" | -## HTTP Metrics +## OpenTelemetry Semantic Conventions -### EntryPoint Metrics - -| Metric | Type | [Labels](#labels) | Description | -|-----------------------|-----------|--------------------------------------------|---------------------------------------------------------------------| -| Requests total | Count | `code`, `method`, `protocol`, `entrypoint` | The total count of HTTP requests received by an entrypoint. | -| Requests TLS total | Count | `tls_version`, `tls_cipher`, `entrypoint` | The total count of HTTPS requests received by an entrypoint. | -| Request duration | Histogram | `code`, `method`, `protocol`, `entrypoint` | Request processing duration histogram on an entrypoint. | -| Requests bytes total | Count | `code`, `method`, `protocol`, `entrypoint` | The total size of HTTP requests in bytes handled by an entrypoint. | -| Responses bytes total | Count | `code`, `method`, `protocol`, `entrypoint` | The total size of HTTP responses in bytes handled by an entrypoint. | - -```prom tab="Prometheus" -traefik_entrypoint_requests_total -traefik_entrypoint_requests_tls_total -traefik_entrypoint_request_duration_seconds -traefik_entrypoint_requests_bytes_total -traefik_entrypoint_responses_bytes_total -``` - -```dd tab="Datadog" -entrypoint.request.total -entrypoint.request.tls.total -entrypoint.request.duration -entrypoint.requests.bytes.total -entrypoint.responses.bytes.total -``` - -```influxdb tab="InfluxDB2" -traefik.entrypoint.requests.total -traefik.entrypoint.requests.tls.total -traefik.entrypoint.request.duration -traefik.entrypoint.requests.bytes.total -traefik.entrypoint.responses.bytes.total -``` - -```statsd tab="StatsD" -# Default prefix: "traefik" -{prefix}.entrypoint.request.total -{prefix}.entrypoint.request.tls.total -{prefix}.entrypoint.request.duration -{prefix}.entrypoint.requests.bytes.total -{prefix}.entrypoint.responses.bytes.total -``` - -```opentelemetry tab="OpenTelemetry" -traefik_entrypoint_requests_total -traefik_entrypoint_requests_tls_total -traefik_entrypoint_request_duration_seconds -traefik_entrypoint_requests_bytes_total -traefik_entrypoint_responses_bytes_total -``` - -### Router Metrics - -| Metric | Type | [Labels](#labels) | Description | -|-----------------------|-----------|---------------------------------------------------|----------------------------------------------------------------| -| Requests total | Count | `code`, `method`, `protocol`, `router`, `service` | The total count of HTTP requests handled by a router. | -| Requests TLS total | Count | `tls_version`, `tls_cipher`, `router`, `service` | The total count of HTTPS requests handled by a router. | -| Request duration | Histogram | `code`, `method`, `protocol`, `router`, `service` | Request processing duration histogram on a router. | -| Requests bytes total | Count | `code`, `method`, `protocol`, `router`, `service` | The total size of HTTP requests in bytes handled by a router. | -| Responses bytes total | Count | `code`, `method`, `protocol`, `router`, `service` | The total size of HTTP responses in bytes handled by a router. | - -```prom tab="Prometheus" -traefik_router_requests_total -traefik_router_requests_tls_total -traefik_router_request_duration_seconds -traefik_router_requests_bytes_total -traefik_router_responses_bytes_total -``` - -```dd tab="Datadog" -router.request.total -router.request.tls.total -router.request.duration -router.requests.bytes.total -router.responses.bytes.total -``` - -```influxdb tab="InfluxDB2" -traefik.router.requests.total -traefik.router.requests.tls.total -traefik.router.request.duration -traefik.router.requests.bytes.total -traefik.router.responses.bytes.total -``` - -```statsd tab="StatsD" -# Default prefix: "traefik" -{prefix}.router.request.total -{prefix}.router.request.tls.total -{prefix}.router.request.duration -{prefix}.router.requests.bytes.total -{prefix}.router.responses.bytes.total -``` - -```opentelemetry tab="OpenTelemetry" -traefik_router_requests_total -traefik_router_requests_tls_total -traefik_router_request_duration_seconds -traefik_router_requests_bytes_total -traefik_router_responses_bytes_total -``` - -### Service Metrics - -| Metric | Type | Labels | Description | -|-----------------------|-----------|-----------------------------------------|-------------------------------------------------------------| -| Requests total | Count | `code`, `method`, `protocol`, `service` | The total count of HTTP requests processed on a service. | -| Requests TLS total | Count | `tls_version`, `tls_cipher`, `service` | The total count of HTTPS requests processed on a service. | -| Request duration | Histogram | `code`, `method`, `protocol`, `service` | Request processing duration histogram on a service. | -| Retries total | Count | `service` | The count of requests retries on a service. | -| Server UP | Gauge | `service`, `url` | Current service's server status, 0 for a down or 1 for up. | -| Requests bytes total | Count | `code`, `method`, `protocol`, `service` | The total size of requests in bytes received by a service. | -| Responses bytes total | Count | `code`, `method`, `protocol`, `service` | The total size of responses in bytes returned by a service. | - -```prom tab="Prometheus" -traefik_service_requests_total -traefik_service_requests_tls_total -traefik_service_request_duration_seconds -traefik_service_retries_total -traefik_service_server_up -traefik_service_requests_bytes_total -traefik_service_responses_bytes_total -``` - -```dd tab="Datadog" -service.request.total -router.service.tls.total -service.request.duration -service.retries.total -service.server.up -service.requests.bytes.total -service.responses.bytes.total -``` - -```influxdb tab="InfluxDB2" -traefik.service.requests.total -traefik.service.requests.tls.total -traefik.service.request.duration -traefik.service.retries.total -traefik.service.server.up -traefik.service.requests.bytes.total -traefik.service.responses.bytes.total -``` - -```statsd tab="StatsD" -# Default prefix: "traefik" -{prefix}.service.request.total -{prefix}.service.request.tls.total -{prefix}.service.request.duration -{prefix}.service.retries.total -{prefix}.service.server.up -{prefix}.service.requests.bytes.total -{prefix}.service.responses.bytes.total -``` - -```opentelemetry tab="OpenTelemetry" -traefik_service_requests_total -traefik_service_requests_tls_total -traefik_service_request_duration_seconds -traefik_service_retries_total -traefik_service_server_up -traefik_service_requests_bytes_total -traefik_service_responses_bytes_total -``` - -### Labels - -Here is a comprehensive list of labels that are provided by the metrics: - -| Label | Description | example | -|---------------|---------------------------------------|----------------------------| -| `cn` | Certificate Common Name | "example.com" | -| `code` | Request code | "200" | -| `entrypoint` | Entrypoint that handled the request | "example_entrypoint" | -| `method` | Request Method | "GET" | -| `protocol` | Request protocol | "http" | -| `router` | Router that handled the request | "example_router" | -| `sans` | Certificate Subject Alternative NameS | "example.com" | -| `serial` | Certificate Serial Number | "123..." | -| `service` | Service that handled the request | "example_service@provider" | -| `tls_cipher` | TLS cipher used for the request | "TLS_FALLBACK_SCSV" | -| `tls_version` | TLS version used for the request | "1.0" | -| `url` | Service server url | "http://example.com" | - -!!! info "`method` label value" - - If the HTTP method verb on a request is not one defined in the set of common methods for [`HTTP/1.1`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) - or the [`PRI`](https://datatracker.ietf.org/doc/html/rfc7540#section-11.6) verb (for `HTTP/2`), - then the value for the method label becomes `EXTENSION_METHOD`. - -## Semantic Conventions for HTTP Metrics - -Traefik Proxy follows [official OTLP semantic conventions v1.23.1](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.1/docs/http/http-metrics.md). +Traefik Proxy follows [official OpenTelemetry semantic conventions v1.23.1](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.1/docs/http/http-metrics.md). ### HTTP Server @@ -328,3 +136,197 @@ Here is a comprehensive list of labels that are provided by the metrics: | `server.address` | Name of the local HTTP server that received the request | "example.com" | | `server.port` | Port of the local HTTP server that received the request | "80" | | `url.scheme` | The URI scheme component identifying the used protocol | "http" | + +## HTTP Metrics + +On top of the official OpenTelemetry semantic conventions, Traefik provides its own metrics to monitor the incoming traffic. + +### EntryPoint Metrics + +| Metric | Type | [Labels](#labels) | Description | +|-----------------------|-----------|--------------------------------------------|---------------------------------------------------------------------| +| Requests total | Count | `code`, `method`, `protocol`, `entrypoint` | The total count of HTTP requests received by an entrypoint. | +| Requests TLS total | Count | `tls_version`, `tls_cipher`, `entrypoint` | The total count of HTTPS requests received by an entrypoint. | +| Request duration | Histogram | `code`, `method`, `protocol`, `entrypoint` | Request processing duration histogram on an entrypoint. | +| Requests bytes total | Count | `code`, `method`, `protocol`, `entrypoint` | The total size of HTTP requests in bytes handled by an entrypoint. | +| Responses bytes total | Count | `code`, `method`, `protocol`, `entrypoint` | The total size of HTTP responses in bytes handled by an entrypoint. | + +```opentelemetry tab="OpenTelemetry" +traefik_entrypoint_requests_total +traefik_entrypoint_requests_tls_total +traefik_entrypoint_request_duration_seconds +traefik_entrypoint_requests_bytes_total +traefik_entrypoint_responses_bytes_total +``` + +```prom tab="Prometheus" +traefik_entrypoint_requests_total +traefik_entrypoint_requests_tls_total +traefik_entrypoint_request_duration_seconds +traefik_entrypoint_requests_bytes_total +traefik_entrypoint_responses_bytes_total +``` + +```dd tab="Datadog" +entrypoint.request.total +entrypoint.request.tls.total +entrypoint.request.duration +entrypoint.requests.bytes.total +entrypoint.responses.bytes.total +``` + +```influxdb tab="InfluxDB2" +traefik.entrypoint.requests.total +traefik.entrypoint.requests.tls.total +traefik.entrypoint.request.duration +traefik.entrypoint.requests.bytes.total +traefik.entrypoint.responses.bytes.total +``` + +```statsd tab="StatsD" +# Default prefix: "traefik" +{prefix}.entrypoint.request.total +{prefix}.entrypoint.request.tls.total +{prefix}.entrypoint.request.duration +{prefix}.entrypoint.requests.bytes.total +{prefix}.entrypoint.responses.bytes.total +``` + +### Router Metrics + +| Metric | Type | [Labels](#labels) | Description | +|-----------------------|-----------|---------------------------------------------------|----------------------------------------------------------------| +| Requests total | Count | `code`, `method`, `protocol`, `router`, `service` | The total count of HTTP requests handled by a router. | +| Requests TLS total | Count | `tls_version`, `tls_cipher`, `router`, `service` | The total count of HTTPS requests handled by a router. | +| Request duration | Histogram | `code`, `method`, `protocol`, `router`, `service` | Request processing duration histogram on a router. | +| Requests bytes total | Count | `code`, `method`, `protocol`, `router`, `service` | The total size of HTTP requests in bytes handled by a router. | +| Responses bytes total | Count | `code`, `method`, `protocol`, `router`, `service` | The total size of HTTP responses in bytes handled by a router. | + +```opentelemetry tab="OpenTelemetry" +traefik_router_requests_total +traefik_router_requests_tls_total +traefik_router_request_duration_seconds +traefik_router_requests_bytes_total +traefik_router_responses_bytes_total +``` + +```prom tab="Prometheus" +traefik_router_requests_total +traefik_router_requests_tls_total +traefik_router_request_duration_seconds +traefik_router_requests_bytes_total +traefik_router_responses_bytes_total +``` + +```dd tab="Datadog" +router.request.total +router.request.tls.total +router.request.duration +router.requests.bytes.total +router.responses.bytes.total +``` + +```influxdb tab="InfluxDB2" +traefik.router.requests.total +traefik.router.requests.tls.total +traefik.router.request.duration +traefik.router.requests.bytes.total +traefik.router.responses.bytes.total +``` + +```statsd tab="StatsD" +# Default prefix: "traefik" +{prefix}.router.request.total +{prefix}.router.request.tls.total +{prefix}.router.request.duration +{prefix}.router.requests.bytes.total +{prefix}.router.responses.bytes.total +``` + +### Service Metrics + +| Metric | Type | Labels | Description | +|-----------------------|-----------|-----------------------------------------|-------------------------------------------------------------| +| Requests total | Count | `code`, `method`, `protocol`, `service` | The total count of HTTP requests processed on a service. | +| Requests TLS total | Count | `tls_version`, `tls_cipher`, `service` | The total count of HTTPS requests processed on a service. | +| Request duration | Histogram | `code`, `method`, `protocol`, `service` | Request processing duration histogram on a service. | +| Retries total | Count | `service` | The count of requests retries on a service. | +| Server UP | Gauge | `service`, `url` | Current service's server status, 0 for a down or 1 for up. | +| Requests bytes total | Count | `code`, `method`, `protocol`, `service` | The total size of requests in bytes received by a service. | +| Responses bytes total | Count | `code`, `method`, `protocol`, `service` | The total size of responses in bytes returned by a service. | + +```opentelemetry tab="OpenTelemetry" +traefik_service_requests_total +traefik_service_requests_tls_total +traefik_service_request_duration_seconds +traefik_service_retries_total +traefik_service_server_up +traefik_service_requests_bytes_total +traefik_service_responses_bytes_total +``` + +```prom tab="Prometheus" +traefik_service_requests_total +traefik_service_requests_tls_total +traefik_service_request_duration_seconds +traefik_service_retries_total +traefik_service_server_up +traefik_service_requests_bytes_total +traefik_service_responses_bytes_total +``` + +```dd tab="Datadog" +service.request.total +router.service.tls.total +service.request.duration +service.retries.total +service.server.up +service.requests.bytes.total +service.responses.bytes.total +``` + +```influxdb tab="InfluxDB2" +traefik.service.requests.total +traefik.service.requests.tls.total +traefik.service.request.duration +traefik.service.retries.total +traefik.service.server.up +traefik.service.requests.bytes.total +traefik.service.responses.bytes.total +``` + +```statsd tab="StatsD" +# Default prefix: "traefik" +{prefix}.service.request.total +{prefix}.service.request.tls.total +{prefix}.service.request.duration +{prefix}.service.retries.total +{prefix}.service.server.up +{prefix}.service.requests.bytes.total +{prefix}.service.responses.bytes.total +``` + +### Labels + +Here is a comprehensive list of labels that are provided by the metrics: + +| Label | Description | example | +|---------------|---------------------------------------|----------------------------| +| `cn` | Certificate Common Name | "example.com" | +| `code` | Request code | "200" | +| `entrypoint` | Entrypoint that handled the request | "example_entrypoint" | +| `method` | Request Method | "GET" | +| `protocol` | Request protocol | "http" | +| `router` | Router that handled the request | "example_router" | +| `sans` | Certificate Subject Alternative NameS | "example.com" | +| `serial` | Certificate Serial Number | "123..." | +| `service` | Service that handled the request | "example_service@provider" | +| `tls_cipher` | TLS cipher used for the request | "TLS_FALLBACK_SCSV" | +| `tls_version` | TLS version used for the request | "1.0" | +| `url` | Service server url | "http://example.com" | + +!!! info "`method` label value" + + If the HTTP method verb on a request is not one defined in the set of common methods for [`HTTP/1.1`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) + or the [`PRI`](https://datatracker.ietf.org/doc/html/rfc7540#section-11.6) verb (for `HTTP/2`), + then the value for the method label becomes `EXTENSION_METHOD`. diff --git a/docs/content/observability/overview.md b/docs/content/observability/overview.md index 2de429b4e..f5f46bbf3 100644 --- a/docs/content/observability/overview.md +++ b/docs/content/observability/overview.md @@ -29,7 +29,7 @@ Read the [Access Logs documentation](./access-logs.md) to learn how to configure Traefik offers a metrics feature that provides valuable insights about the performance and usage. These metrics include the number of requests received, the requests duration, and more. -Traefik supports these metrics systems: Prometheus, Datadog, InfluxDB 2.X, and StatsD. +On top of supporting metrics in the OpenTelemetry format, Traefik supports the following vendor specific metrics systems: Prometheus, Datadog, InfluxDB 2.X, and StatsD. Read the [Metrics documentation](./metrics/overview.md) to learn how to configure it. @@ -37,6 +37,6 @@ Read the [Metrics documentation](./metrics/overview.md) to learn how to configur The Traefik tracing system allows developers to gain deep visibility into the flow of requests through their infrastructure. -Traefik supports these tracing with OpenTelemetry. +Traefik provides tracing information in the OpenTelemery format. Read the [Tracing documentation](./tracing/overview.md) to learn how to configure it. From 42920595ad6b975a47efd3f2eabbe8c951d28f31 Mon Sep 17 00:00:00 2001 From: Fontany--Legall Brandon Date: Fri, 17 May 2024 16:18:04 +0200 Subject: [PATCH 18/26] Display of Content Security Policy values getting out of screen --- webui/src/components/_commons/PanelMiddlewares.vue | 11 +++++------ webui/src/css/sass/app.scss | 8 ++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index c1e03fcee..5ef836118 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -809,12 +809,11 @@

Content Security Policy
- - {{ exData(middleware).contentSecurityPolicy }} - + + + {{ exData(middleware).contentSecurityPolicy }} + + diff --git a/webui/src/css/sass/app.scss b/webui/src/css/sass/app.scss index d73156655..7157a171f 100644 --- a/webui/src/css/sass/app.scss +++ b/webui/src/css/sass/app.scss @@ -121,6 +121,14 @@ body { border-radius: 8px; } +.app-card-as-chip { + box-shadow: none; + + .q-card__section { + padding: 5px !important; + } +} + // Chips .app-chip { border-radius: 8px; From 440cb112500dc87ea22667670f91e199c5756465 Mon Sep 17 00:00:00 2001 From: David <21027243+davidbaptista@users.noreply.github.com> Date: Tue, 21 May 2024 09:24:08 +0200 Subject: [PATCH 19/26] Add support for IP White list --- webui/src/components/_commons/PanelMiddlewares.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index 4b2c24678..dd261f7cf 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -945,8 +945,8 @@ - - + +
@@ -963,8 +963,8 @@
- - + +
From 5e4dc783c7bb42832286dcdc645590a6fe4835a6 Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 21 May 2024 10:42:04 +0200 Subject: [PATCH 20/26] Allow empty configuration for OpenTelemetry metrics and tracing --- .../reference/static-configuration/cli-ref.md | 12 ++++++++++++ .../reference/static-configuration/env-ref.md | 12 ++++++++++++ pkg/tracing/opentelemetry/opentelemetry.go | 4 ++-- pkg/types/metrics.go | 4 ++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 99065ee63..136f8dc8e 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -339,6 +339,9 @@ Enable metrics on services. (Default: ```true```) `--metrics.otlp.explicitboundaries`: Boundaries for latency metrics. (Default: ```0.005000, 0.010000, 0.025000, 0.050000, 0.075000, 0.100000, 0.250000, 0.500000, 0.750000, 1.000000, 2.500000, 5.000000, 7.500000, 10.000000```) +`--metrics.otlp.grpc`: +gRPC configuration for the OpenTelemetry collector. (Default: ```false```) + `--metrics.otlp.grpc.endpoint`: Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```) @@ -360,6 +363,9 @@ TLS insecure skip verify (Default: ```false```) `--metrics.otlp.grpc.tls.key`: TLS key +`--metrics.otlp.http`: +HTTP configuration for the OpenTelemetry collector. (Default: ```false```) + `--metrics.otlp.http.endpoint`: Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```) @@ -1056,6 +1062,9 @@ Defines additional attributes (key:value) on all spans. `--tracing.otlp`: Settings for OpenTelemetry. (Default: ```false```) +`--tracing.otlp.grpc`: +gRPC configuration for the OpenTelemetry collector. (Default: ```false```) + `--tracing.otlp.grpc.endpoint`: Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```) @@ -1077,6 +1086,9 @@ TLS insecure skip verify (Default: ```false```) `--tracing.otlp.grpc.tls.key`: TLS key +`--tracing.otlp.http`: +HTTP configuration for the OpenTelemetry collector. (Default: ```false```) + `--tracing.otlp.http.endpoint`: Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 5d8313abb..12636602c 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -339,6 +339,9 @@ Enable metrics on services. (Default: ```true```) `TRAEFIK_METRICS_OTLP_EXPLICITBOUNDARIES`: Boundaries for latency metrics. (Default: ```0.005000, 0.010000, 0.025000, 0.050000, 0.075000, 0.100000, 0.250000, 0.500000, 0.750000, 1.000000, 2.500000, 5.000000, 7.500000, 10.000000```) +`TRAEFIK_METRICS_OTLP_GRPC`: +gRPC configuration for the OpenTelemetry collector. (Default: ```false```) + `TRAEFIK_METRICS_OTLP_GRPC_ENDPOINT`: Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```) @@ -360,6 +363,9 @@ TLS insecure skip verify (Default: ```false```) `TRAEFIK_METRICS_OTLP_GRPC_TLS_KEY`: TLS key +`TRAEFIK_METRICS_OTLP_HTTP`: +HTTP configuration for the OpenTelemetry collector. (Default: ```false```) + `TRAEFIK_METRICS_OTLP_HTTP_ENDPOINT`: Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```) @@ -1056,6 +1062,9 @@ Defines additional attributes (key:value) on all spans. `TRAEFIK_TRACING_OTLP`: Settings for OpenTelemetry. (Default: ```false```) +`TRAEFIK_TRACING_OTLP_GRPC`: +gRPC configuration for the OpenTelemetry collector. (Default: ```false```) + `TRAEFIK_TRACING_OTLP_GRPC_ENDPOINT`: Sets the gRPC endpoint (host:port) of the collector. (Default: ```localhost:4317```) @@ -1077,6 +1086,9 @@ TLS insecure skip verify (Default: ```false```) `TRAEFIK_TRACING_OTLP_GRPC_TLS_KEY`: TLS key +`TRAEFIK_TRACING_OTLP_HTTP`: +HTTP configuration for the OpenTelemetry collector. (Default: ```false```) + `TRAEFIK_TRACING_OTLP_HTTP_ENDPOINT`: Sets the HTTP endpoint (scheme://host:port/path) of the collector. (Default: ```https://localhost:4318```) diff --git a/pkg/tracing/opentelemetry/opentelemetry.go b/pkg/tracing/opentelemetry/opentelemetry.go index 3806ffcd4..35f1a5f6a 100644 --- a/pkg/tracing/opentelemetry/opentelemetry.go +++ b/pkg/tracing/opentelemetry/opentelemetry.go @@ -26,8 +26,8 @@ import ( // Config provides configuration settings for the open-telemetry tracer. type Config struct { - GRPC *types.OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" export:"true"` - HTTP *types.OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" export:"true"` + GRPC *types.OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + HTTP *types.OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` } // SetDefaults sets the default values. diff --git a/pkg/types/metrics.go b/pkg/types/metrics.go index cde08e189..b97ae9221 100644 --- a/pkg/types/metrics.go +++ b/pkg/types/metrics.go @@ -108,8 +108,8 @@ func (i *InfluxDB2) SetDefaults() { // OTLP contains specific configuration used by the OpenTelemetry Metrics exporter. type OTLP struct { - GRPC *OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" export:"true"` - HTTP *OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" export:"true"` + GRPC *OtelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + HTTP *OtelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"` AddRoutersLabels bool `description:"Enable metrics on routers." json:"addRoutersLabels,omitempty" toml:"addRoutersLabels,omitempty" yaml:"addRoutersLabels,omitempty" export:"true"` From d4d23dce72a496afae7aecfd48f0e186d0690890 Mon Sep 17 00:00:00 2001 From: Dmitry Romashov Date: Tue, 21 May 2024 15:26:04 +0200 Subject: [PATCH 21/26] Fix UI unit tests --- webui/.eslintrc.cjs | 3 - webui/package.json | 13 +- webui/quasar.conf.js | 52 +- webui/quasar.extensions.json | 5 + webui/readme.md | 4 +- ...tions.spec.js => mutations.vitest.spec.js} | 2 +- ...tions.spec.js => mutations.vitest.spec.js} | 2 +- ...tions.spec.js => mutations.vitest.spec.js} | 2 +- webui/test/vitest/setup-file.js | 1 + webui/vitest.config.mjs | 24 + webui/yarn.lock | 1064 +++++++++++++---- 11 files changed, 907 insertions(+), 265 deletions(-) create mode 100644 webui/quasar.extensions.json rename webui/src/store/http/{mutations.spec.js => mutations.vitest.spec.js} (99%) rename webui/src/store/tcp/{mutations.spec.js => mutations.vitest.spec.js} (99%) rename webui/src/store/udp/{mutations.spec.js => mutations.vitest.spec.js} (99%) create mode 100644 webui/test/vitest/setup-file.js create mode 100644 webui/vitest.config.mjs diff --git a/webui/.eslintrc.cjs b/webui/.eslintrc.cjs index c08b11557..331093689 100644 --- a/webui/.eslintrc.cjs +++ b/webui/.eslintrc.cjs @@ -9,7 +9,6 @@ module.exports = { env: { node: true, browser: true, - mocha: true, 'vue/setup-compiler-macros': true }, @@ -18,14 +17,12 @@ module.exports = { // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 'plugin:vue/vue3-essential', 'plugin:vue/vue3-recommended', - 'plugin:mocha/recommended', 'standard' ], // required to lint *.vue files plugins: [ 'vue', - 'mocha' ], globals: { diff --git a/webui/package.json b/webui/package.json index aa8090eae..f33140de3 100644 --- a/webui/package.json +++ b/webui/package.json @@ -8,12 +8,14 @@ "scripts": { "transfer": "node dev/scripts/transfer.js", "lint": "eslint --ext .js,.vue src", - "test-unit": "mocha-webpack --mode=production './src/**/*.spec.js'", "dev": "export APP_ENV='development' && quasar dev", "build-quasar": "quasar build", "build-staging": "export NODE_ENV='production' && export APP_ENV='development' && yarn build-quasar", "build": "export NODE_ENV='production' && export APP_ENV='production' && yarn build-quasar && yarn transfer spa", - "build:nc": "yarn build" + "build:nc": "yarn build", + "test": "echo \"See package.json => scripts for available tests.\" && exit 0", + "test:unit": "vitest", + "test:unit:ci": "vitest run" }, "dependencies": { "@quasar/extras": "^1.16.9", @@ -39,18 +41,17 @@ "@babel/eslint-parser": "^7.23.10", "@quasar/app-vite": "^1.4.3", "@quasar/babel-preset-app": "^2.0.2", + "@quasar/quasar-app-extension-testing-unit-vitest": "^1.0.0", "@vue/test-utils": "^2.4.4", "autoprefixer": "^10.4.2", - "chai": "5.0.3", "eslint": "^8.11.0", "eslint-config-standard": "^17.0.0", "eslint-plugin-import": "^2.19.1", - "eslint-plugin-mocha": "^10.2.0", "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-vue": "^9.0.0", - "mocha": "^10.2.0", - "postcss": "^8.4.14" + "postcss": "^8.4.14", + "vitest": "^1.3.1" }, "engines": { "node": "^20 || ^18 || ^16", diff --git a/webui/quasar.conf.js b/webui/quasar.conf.js index 930136d3a..58a8e2c39 100644 --- a/webui/quasar.conf.js +++ b/webui/quasar.conf.js @@ -5,7 +5,7 @@ const { configure } = require('quasar/wrappers') module.exports = configure(function (ctx) { return { - eslint: { + eslint: { warnings: true, errors: true }, @@ -122,10 +122,10 @@ module.exports = configure(function (ctx) { build: { // Needed to have relative assets in the index.html // https://github.com/quasarframework/quasar/issues/8513#issuecomment-1127654470 - extendViteConf(viteConf, {isServer, isClient}) { - viteConf.base = ""; + extendViteConf (viteConf, { isServer, isClient }) { + viteConf.base = '' }, - viteVuePluginOptions: { + viteVuePluginOptions: { template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('hub-') @@ -139,13 +139,13 @@ module.exports = configure(function (ctx) { publicPath: process.env.APP_PUBLIC_PATH || '', env: process.env.APP_ENV === 'development' ? { // staging: - APP_ENV: process.env.APP_ENV, - APP_API: process.env.APP_API || '/api' - } + APP_ENV: process.env.APP_ENV, + APP_API: process.env.APP_API || '/api' + } : { // production: - APP_ENV: process.env.APP_ENV, - APP_API: process.env.APP_API || '/api' - }, + APP_ENV: process.env.APP_ENV, + APP_API: process.env.APP_API || '/api' + }, uglifyOptions: { compress: { drop_console: process.env.APP_ENV === 'production', @@ -173,7 +173,7 @@ module.exports = configure(function (ctx) { animations: [], ssr: { - pwa: false, + pwa: false }, pwa: { @@ -201,29 +201,29 @@ module.exports = configure(function (ctx) { theme_color: '#027be3', icons: [ { - 'src': 'icons/icon-128x128.png', - 'sizes': '128x128', - 'type': 'image/png' + src: 'icons/icon-128x128.png', + sizes: '128x128', + type: 'image/png' }, { - 'src': 'icons/icon-192x192.png', - 'sizes': '192x192', - 'type': 'image/png' + src: 'icons/icon-192x192.png', + sizes: '192x192', + type: 'image/png' }, { - 'src': 'icons/icon-256x256.png', - 'sizes': '256x256', - 'type': 'image/png' + src: 'icons/icon-256x256.png', + sizes: '256x256', + type: 'image/png' }, { - 'src': 'icons/icon-384x384.png', - 'sizes': '384x384', - 'type': 'image/png' + src: 'icons/icon-384x384.png', + sizes: '384x384', + type: 'image/png' }, { - 'src': 'icons/icon-512x512.png', - 'sizes': '512x512', - 'type': 'image/png' + src: 'icons/icon-512x512.png', + sizes: '512x512', + type: 'image/png' } ] } diff --git a/webui/quasar.extensions.json b/webui/quasar.extensions.json new file mode 100644 index 000000000..69a09257c --- /dev/null +++ b/webui/quasar.extensions.json @@ -0,0 +1,5 @@ +{ + "@quasar/testing-unit-vitest": { + "options": [] + } +} diff --git a/webui/readme.md b/webui/readme.md index 5bd9cd3ad..e621e4080 100644 --- a/webui/readme.md +++ b/webui/readme.md @@ -20,7 +20,7 @@ make clean-webui generate-webui # Generate static contents in `webui/static/` fo ## How to build (only for frontend developer) -- prerequisite: [Node 12.11+](https://nodejs.org) [Yarn](https://yarnpkg.com/) +- prerequisite: [Node 20.11+](https://nodejs.org) [Yarn 1.22.19](https://yarnpkg.com/) - Go to the `webui/` directory @@ -57,7 +57,7 @@ make clean-webui generate-webui # Generate static contents in `webui/static/` fo - [Node](https://nodejs.org) - [Yarn](https://yarnpkg.com/) -- [Webpack](https://github.com/webpack/webpack) +- [Quasar](https://quasar.dev/) - [Vue](https://vuejs.org/) - [Bulma](https://bulma.io) - [D3](https://d3js.org) diff --git a/webui/src/store/http/mutations.spec.js b/webui/src/store/http/mutations.vitest.spec.js similarity index 99% rename from webui/src/store/http/mutations.spec.js rename to webui/src/store/http/mutations.vitest.spec.js index 890fd1406..125216186 100644 --- a/webui/src/store/http/mutations.spec.js +++ b/webui/src/store/http/mutations.vitest.spec.js @@ -1,4 +1,4 @@ -import { expect } from 'chai' +import { describe, expect, it } from 'vitest' import store from './index.js' const { diff --git a/webui/src/store/tcp/mutations.spec.js b/webui/src/store/tcp/mutations.vitest.spec.js similarity index 99% rename from webui/src/store/tcp/mutations.spec.js rename to webui/src/store/tcp/mutations.vitest.spec.js index d9f0555cb..17dc4ff9a 100644 --- a/webui/src/store/tcp/mutations.spec.js +++ b/webui/src/store/tcp/mutations.vitest.spec.js @@ -1,4 +1,4 @@ -import { expect } from 'chai' +import { describe, expect, it } from 'vitest' import store from './index.js' const { diff --git a/webui/src/store/udp/mutations.spec.js b/webui/src/store/udp/mutations.vitest.spec.js similarity index 99% rename from webui/src/store/udp/mutations.spec.js rename to webui/src/store/udp/mutations.vitest.spec.js index fa09e3d74..4c6b39f63 100644 --- a/webui/src/store/udp/mutations.spec.js +++ b/webui/src/store/udp/mutations.vitest.spec.js @@ -1,4 +1,4 @@ -import { expect } from 'chai' +import { describe, expect, it } from 'vitest' import store from './index.js' const { diff --git a/webui/test/vitest/setup-file.js b/webui/test/vitest/setup-file.js new file mode 100644 index 000000000..499204981 --- /dev/null +++ b/webui/test/vitest/setup-file.js @@ -0,0 +1 @@ +// This file will be run before each test file diff --git a/webui/vitest.config.mjs b/webui/vitest.config.mjs new file mode 100644 index 000000000..bb3c6d45d --- /dev/null +++ b/webui/vitest.config.mjs @@ -0,0 +1,24 @@ +import { defineConfig } from 'vitest/config'; +import vue from '@vitejs/plugin-vue'; +import { quasar, transformAssetUrls } from '@quasar/vite-plugin'; +import jsconfigPaths from 'vite-jsconfig-paths'; + +// https://vitejs.dev/config/ +export default defineConfig({ + test: { + environment: 'happy-dom', + setupFiles: 'test/vitest/setup-file.js', + include: [ + // Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__' + // Matches all files with extension 'js', 'jsx', 'ts' and 'tsx' + 'src/**/*.vitest.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', + ], + }, + plugins: [ + vue({ + template: { transformAssetUrls }, + }), + quasar(), + jsconfigPaths(), + ], +}); diff --git a/webui/yarn.lock b/webui/yarn.lock index 8edafadd5..0534b50ba 100644 --- a/webui/yarn.lock +++ b/webui/yarn.lock @@ -1047,11 +1047,131 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@cush/relative@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@cush/relative/-/relative-1.0.0.tgz#8cd1769bf9bde3bb27dac356b1bc94af40f6cc16" + integrity sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA== + +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + "@esbuild/linux-loong64@0.14.54": version "0.14.54" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1115,6 +1235,13 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" @@ -1261,6 +1388,16 @@ resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.9.tgz#6cdf9d34862e6f58009443a2d1e34b84a5e73ad0" integrity sha512-SlOhwzXyPQHWgQIS2ncyDdYdksCJvUYNtgsDQqzAKEG3r3d/ejOxvThle79HTK3Q6HB+gQWFG21Ux00Osr5XSw== +"@quasar/quasar-app-extension-testing-unit-vitest@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@quasar/quasar-app-extension-testing-unit-vitest/-/quasar-app-extension-testing-unit-vitest-1.0.0.tgz#7c3a5603dae4dd10a89e81d06591b438b5b134a1" + integrity sha512-nXvwPUyZEnCCUZSjYmwXL6Gl8R4jDtqv7cpMcm2hwxryF7SQlK8FwBtOnjWdN/CEfelFG+UAowz3OHuN+ZoB6A== + dependencies: + happy-dom "^13.6.2" + lodash-es "^4.17.21" + vite-jsconfig-paths "^2.0.1" + vite-tsconfig-paths "^4.3.1" + "@quasar/render-ssr-error@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@quasar/render-ssr-error/-/render-ssr-error-1.0.3.tgz#33f27231007d1b222de41d3d70c29a6d14f9498a" @@ -1281,6 +1418,91 @@ estree-walker "^2.0.1" picomatch "^2.2.2" +"@rollup/rollup-android-arm-eabi@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz#bddf05c3387d02fac04b6b86b3a779337edfed75" + integrity sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g== + +"@rollup/rollup-android-arm64@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz#b26bd09de58704c0a45e3375b76796f6eda825e4" + integrity sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ== + +"@rollup/rollup-darwin-arm64@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz#c5f3fd1aa285b6d33dda6e3f3ca395f8c37fd5ca" + integrity sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA== + +"@rollup/rollup-darwin-x64@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz#8e4673734d7dc9d68f6d48e81246055cda0e840f" + integrity sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw== + +"@rollup/rollup-linux-arm-gnueabihf@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz#53ed38eb13b58ababdb55a7f66f0538a7f85dcba" + integrity sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw== + +"@rollup/rollup-linux-arm-musleabihf@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz#0706ee38330e267a5c9326956820f009cfb21fcd" + integrity sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw== + +"@rollup/rollup-linux-arm64-gnu@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz#426fce7b8b242ac5abd48a10a5020f5a468c6cb4" + integrity sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA== + +"@rollup/rollup-linux-arm64-musl@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz#65bf944530d759b50d7ffd00dfbdf4125a43406f" + integrity sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz#494ba3b31095e9a45df9c3f646d21400fb631a95" + integrity sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw== + +"@rollup/rollup-linux-riscv64-gnu@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz#8b88ed0a40724cce04aa15374ebe5ba4092d679f" + integrity sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ== + +"@rollup/rollup-linux-s390x-gnu@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz#09c9e5ec57a0f6ec3551272c860bb9a04b96d70f" + integrity sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg== + +"@rollup/rollup-linux-x64-gnu@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz#197f27fd481ad9c861021d5cbbf21793922a631c" + integrity sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA== + +"@rollup/rollup-linux-x64-musl@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz#5cc0522f4942f2df625e9bfb6fb02c6580ffbce6" + integrity sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg== + +"@rollup/rollup-win32-arm64-msvc@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz#a648122389d23a7543b261fba082e65fefefe4f6" + integrity sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg== + +"@rollup/rollup-win32-ia32-msvc@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz#34727b5c7953c35fc6e1ae4f770ad3a2025f8e03" + integrity sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw== + +"@rollup/rollup-win32-x64-msvc@4.14.3": + version "4.14.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz#5b2fb4d8cd44c05deef8a7b0e6deb9ccb8939d18" + integrity sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -1316,6 +1538,11 @@ resolved "https://registry.yarnpkg.com/@types/cordova/-/cordova-0.0.34.tgz#ea7addf74ecec3d7629827a0c39e2c9addc73d04" integrity sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA== +"@types/estree@1.0.5", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/express-serve-static-core@^4.17.33": version "4.17.43" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" @@ -1422,6 +1649,50 @@ resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz#966a6279060eb2d9d1a02ea1a331af071afdcf9e" integrity sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg== +"@vitest/expect@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.5.0.tgz#961190510a2723bd4abf5540bcec0a4dfd59ef14" + integrity sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA== + dependencies: + "@vitest/spy" "1.5.0" + "@vitest/utils" "1.5.0" + chai "^4.3.10" + +"@vitest/runner@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.5.0.tgz#1f7cb78ee4064e73e53d503a19c1b211c03dfe0c" + integrity sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ== + dependencies: + "@vitest/utils" "1.5.0" + p-limit "^5.0.0" + pathe "^1.1.1" + +"@vitest/snapshot@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.5.0.tgz#cd2d611fd556968ce8fb6b356a09b4593c525947" + integrity sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A== + dependencies: + magic-string "^0.30.5" + pathe "^1.1.1" + pretty-format "^29.7.0" + +"@vitest/spy@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.5.0.tgz#1369a1bec47f46f18eccfa45f1e8fbb9b5e15e77" + integrity sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w== + dependencies: + tinyspy "^2.2.0" + +"@vitest/utils@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.5.0.tgz#90c9951f4516f6d595da24876b58e615f6c99863" + integrity sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A== + dependencies: + diff-sequences "^29.6.3" + estree-walker "^3.0.3" + loupe "^2.3.7" + pretty-format "^29.7.0" + "@vue/compiler-core@3.4.18": version "3.4.18" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.18.tgz#b875e27f6ba71c6e2cf4e9befa7f98c66afabf22" @@ -1532,7 +1803,12 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: +acorn-walk@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + +acorn@^8.11.3, acorn@^8.9.0: version "8.11.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -1571,11 +1847,6 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1607,11 +1878,21 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -1750,10 +2031,10 @@ arraybuffer.prototype.slice@^1.0.2: is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" -assertion-error@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" - integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== astral-regex@^2.0.0: version "2.0.0" @@ -1920,11 +2201,6 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - browserslist@^4.22.2: version "4.22.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" @@ -1970,6 +2246,11 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.6.tgz#6c46675fc7a5e9de82d75a233d586c8b7ac0d931" @@ -1993,26 +2274,23 @@ camel-case@^3.0.0: no-case "^2.2.0" upper-case "^1.1.1" -camelcase@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - caniuse-lite@^1.0.30001578, caniuse-lite@^1.0.30001580: version "1.0.30001585" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001585.tgz#0b4e848d84919c783b2a41c13f7de8ce96744401" integrity sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q== -chai@5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/chai/-/chai-5.0.3.tgz#db8e109373b86e7fb33d3ef0d0116f0fa8019066" - integrity sha512-wKGCtYv2kVY5WEjKqQ3fSIZWtTFveZCtzinhTZbx3/trVkxefiwovhpU9kRVCwxvKKCEjTWXPdM1/T7zPoDgow== +chai@^4.3.10: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" + integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== dependencies: - assertion-error "^2.0.1" - check-error "^2.0.0" - deep-eql "^5.0.1" - loupe "^3.1.0" - pathval "^2.0.0" + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" chalk@^2.4.2: version "2.4.2" @@ -2043,25 +2321,12 @@ chart.js@^4.4.1: dependencies: "@kurkle/color" "^0.3.0" -check-error@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.0.0.tgz#589a4f201b6256fd93a2d165089fe43d2676d8c6" - integrity sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog== - -chokidar@3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" + get-func-name "^2.0.2" "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: version "3.6.0" @@ -2107,15 +2372,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2180,6 +2436,11 @@ commander@^2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + common-path-prefix@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" @@ -2220,6 +2481,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +confbox@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" + integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== + config-chain@^1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -2311,13 +2577,6 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2325,20 +2584,24 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" decode-uri-component@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5" integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== -deep-eql@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.1.tgz#21ea2c0d561a4d08cdd99c417ac584e0fb121385" - integrity sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw== +deep-eql@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" deep-is@^0.1.3: version "0.1.4" @@ -2391,10 +2654,10 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -diff@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== doctrine@^2.1.0: version "2.1.0" @@ -2816,6 +3079,35 @@ esbuild@^0.14.27: esbuild-windows-64 "0.14.54" esbuild-windows-arm64 "0.14.54" +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -2826,16 +3118,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-compat-utils@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz#f45e3b5ced4c746c127cf724fb074cd4e730d653" @@ -2894,14 +3186,6 @@ eslint-plugin-import@^2.19.1: semver "^6.3.1" tsconfig-paths "^3.15.0" -eslint-plugin-mocha@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-10.2.0.tgz#15b05ce5be4b332bb0d76826ec1c5ebf67102ad6" - integrity sha512-ZhdxzSZnd1P9LqDPF0DBcFLpRIGdh1zkF2JHnQklKQOvrQtT73kdP5K9V2mzvbLR+cCAO9OI48NXK/Ax9/ciCQ== - dependencies: - eslint-utils "^3.0.0" - rambda "^7.4.0" - eslint-plugin-n@^16.6.2: version "16.6.2" resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz#6a60a1a376870064c906742272074d5d0b412b0b" @@ -2953,14 +3237,7 @@ eslint-scope@^7.1.1, eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: +eslint-visitor-keys@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== @@ -3052,6 +3329,13 @@ estree-walker@^2.0.1, estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -3062,6 +3346,21 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + express@^4.17.3: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -3196,14 +3495,6 @@ find-cache-dir@^4.0.0: common-path-prefix "^3.0.0" pkg-dir "^7.0.0" -find-up@5.0.0, find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -3211,6 +3502,14 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + find-up@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" @@ -3301,7 +3600,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: +fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3336,7 +3635,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.1: +get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== @@ -3352,6 +3651,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-symbol-description@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -3382,16 +3686,21 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@8.1.0, glob@^8.0.3: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== +glob-regex@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/glob-regex/-/glob-regex-0.3.2.tgz#27348f2f60648ec32a4a53137090b9fb934f3425" + integrity sha512-m5blUd3/OqDTWwzBBtWBPrGlAzatRywHameHeekAZyZrskYouOGdNB8T/q6JucucvJXtOuyHIn0/Yia7iDasDw== + +glob@^10.3.10: + version "10.3.12" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" + integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg== dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" + foreground-child "^3.1.0" + jackspeak "^2.3.6" + minimatch "^9.0.1" + minipass "^7.0.4" + path-scurry "^1.10.2" glob@^10.3.3: version "10.3.10" @@ -3416,6 +3725,17 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.2.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -3435,6 +3755,11 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -3452,6 +3777,15 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +happy-dom@^13.6.2: + version "13.10.1" + resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-13.10.1.tgz#bfaf147704055e7c2feb4069adc899a838588307" + integrity sha512-9GZLEFvQL5EgfJX2zcBgu1nsPUn98JF/EiJnSfQbdxI6YEQGqpd09lXXxOmYonRBIEFz9JlGCOiPflDzgS1p8w== + dependencies: + entities "^4.5.0" + webidl-conversions "^7.0.0" + whatwg-mimetype "^3.0.0" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3498,7 +3832,7 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" -he@1.2.0, he@^1.2.0: +he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -3527,6 +3861,11 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3730,11 +4069,6 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -3757,6 +4091,11 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -3822,7 +4161,7 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -jackspeak@^2.3.5: +jackspeak@^2.3.5, jackspeak@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== @@ -3846,7 +4185,12 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@4.1.0, js-yaml@^4.1.0: +js-tokens@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.0.tgz#0f893996d6f3ed46df7f0a3b12a03f5fd84223c1" + integrity sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== + +js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -3936,6 +4280,19 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +local-pkg@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" + integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== + dependencies: + mlly "^1.4.2" + pkg-types "^1.0.3" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -3958,6 +4315,11 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -4008,7 +4370,7 @@ lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@4.1.0, log-symbols@^4.1.0: +log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -4016,10 +4378,10 @@ log-symbols@4.1.0, log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -loupe@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.0.tgz#46ef1a4ffee73145f5c0a627536d754787c1ea2a" - integrity sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg== +loupe@^2.3.6, loupe@^2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== dependencies: get-func-name "^2.0.1" @@ -4028,6 +4390,11 @@ lower-case@^1.1.1: resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA== +lru-cache@^10.2.0, "lru-cache@^9.1.1 || ^10.0.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4042,10 +4409,12 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== +magic-string@^0.30.5: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" magic-string@^0.30.6: version "0.30.7" @@ -4064,6 +4433,11 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -4104,12 +4478,10 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== - dependencies: - brace-expansion "^2.0.1" +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== minimatch@9.0.1: version "9.0.1" @@ -4144,36 +4516,20 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4: version "7.0.4" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== -mocha@^10.2.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.3.0.tgz#0e185c49e6dccf582035c05fa91084a4ff6e3fe9" - integrity sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg== +mlly@^1.4.2, mlly@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.6.1.tgz#0983067dc3366d6314fc5e12712884e6978d028f" + integrity sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA== dependencies: - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.3" - debug "4.3.4" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "8.1.0" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "5.0.1" - ms "2.1.3" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - workerpool "6.2.1" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" + acorn "^8.11.3" + pathe "^1.1.2" + pkg-types "^1.0.3" + ufo "^1.3.2" moment@^2.30.1: version "2.30.1" @@ -4200,6 +4556,15 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -4244,6 +4609,13 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + nth-check@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -4251,6 +4623,11 @@ nth-check@^2.1.1: dependencies: boolbase "^1.0.0" +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + object-inspect@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" @@ -4326,6 +4703,13 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + open@^8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -4388,6 +4772,13 @@ p-limit@^4.0.0: dependencies: yocto-queue "^1.0.0" +p-limit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-5.0.0.tgz#6946d5b7140b649b7a33a027d89b4c625b3a5985" + integrity sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -4458,6 +4849,11 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -4471,15 +4867,28 @@ path-scurry@^1.10.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" + integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -pathval@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" - integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== +pathe@^1.1.1, pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== picocolors@^1.0.0: version "1.0.0" @@ -4491,6 +4900,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + pkg-dir@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-7.0.0.tgz#8f0c08d6df4476756c5ff29b3282d0bab7517d11" @@ -4498,6 +4912,15 @@ pkg-dir@^7.0.0: dependencies: find-up "^6.3.0" +pkg-types@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.0.tgz#3ec1bf33379030fd0a34c227b6c650e8ea7ca271" + integrity sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA== + dependencies: + confbox "^0.1.7" + mlly "^1.6.1" + pathe "^1.1.2" + pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -4527,11 +4950,29 @@ postcss@^8.4.13, postcss@^8.4.14, postcss@^8.4.33: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -4586,11 +5027,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -rambda@^7.4.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/rambda/-/rambda-7.5.0.tgz#1865044c59bc0b16f63026c6e5a97e4b1bbe98fe" - integrity sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA== - randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -4613,6 +5049,11 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + readable-stream@^2.0.0, readable-stream@^2.0.5: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -4649,6 +5090,17 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recrawl-sync@^2.0.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/recrawl-sync/-/recrawl-sync-2.2.3.tgz#757adcdaae4799466dde5b8ee52122ff9636dfb1" + integrity sha512-vSaTR9t+cpxlskkdUFrsEpnf67kSmPk66yAGT1fZPrDudxQjoMzPgQhSMImQ0pAw5k0NPirefQfhopSjhdUtpQ== + dependencies: + "@cush/relative" "^1.0.0" + glob-regex "^0.3.0" + slash "^3.0.0" + sucrase "^3.20.3" + tslib "^1.9.3" + regenerate-unicode-properties@^10.1.0: version "10.1.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" @@ -4782,6 +5234,31 @@ rollup-plugin-visualizer@^5.5.4: optionalDependencies: fsevents "~2.3.2" +rollup@^4.13.0: + version "4.14.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.14.3.tgz#bcbb7784b35826d3164346fa6d5aac95190d8ba9" + integrity sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.14.3" + "@rollup/rollup-android-arm64" "4.14.3" + "@rollup/rollup-darwin-arm64" "4.14.3" + "@rollup/rollup-darwin-x64" "4.14.3" + "@rollup/rollup-linux-arm-gnueabihf" "4.14.3" + "@rollup/rollup-linux-arm-musleabihf" "4.14.3" + "@rollup/rollup-linux-arm64-gnu" "4.14.3" + "@rollup/rollup-linux-arm64-musl" "4.14.3" + "@rollup/rollup-linux-powerpc64le-gnu" "4.14.3" + "@rollup/rollup-linux-riscv64-gnu" "4.14.3" + "@rollup/rollup-linux-s390x-gnu" "4.14.3" + "@rollup/rollup-linux-x64-gnu" "4.14.3" + "@rollup/rollup-linux-x64-musl" "4.14.3" + "@rollup/rollup-win32-arm64-msvc" "4.14.3" + "@rollup/rollup-win32-ia32-msvc" "4.14.3" + "@rollup/rollup-win32-x64-msvc" "4.14.3" + fsevents "~2.3.2" + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -4890,13 +5367,6 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - serialize-javascript@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -4969,16 +5439,26 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -4993,6 +5473,11 @@ slice-ansi@^4.0.0: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + source-map@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" @@ -5013,11 +5498,21 @@ stack-trace@^1.0.0-pre2: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-1.0.0-pre2.tgz#46a83a79f1b287807e9aaafc6a5dd8bcde626f9c" integrity sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A== +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +std-env@^3.5.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" + integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== + "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -5096,17 +5591,35 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-json-comments@3.1.1, strip-json-comments@^3.1.1: +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== +strip-literal@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-2.1.0.tgz#6d82ade5e2e74f5c7e8739b6c84692bd65f0bd2a" + integrity sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw== dependencies: - has-flag "^4.0.0" + js-tokens "^9.0.0" + +sucrase@^3.20.3: + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "^10.3.10" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" supports-color@^5.3.0: version "5.5.0" @@ -5154,11 +5667,40 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tinybench@^2.5.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.7.0.tgz#d56198a69bead7e240c8f9542484f3eb3c3f749d" + integrity sha512-Qgayeb106x2o4hNzNjsZEfFziw8IbKqtbXBjVh7VIZfBxfD5M4gWtpyx5+YTae2gJ6Y6Dz/KLepiv16RFeQWNA== + +tinypool@^0.8.3: + version "0.8.4" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.4.tgz#e217fe1270d941b39e98c625dcecebb1408c9aa8" + integrity sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ== + +tinyspy@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.1.tgz#117b2342f1f38a0dbdcc73a50a454883adf861d1" + integrity sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -5183,7 +5725,17 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -tsconfig-paths@^3.15.0: +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tsconfck@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.3.tgz#d9bda0e87d05b1c360e996c9050473c7e6f8084f" + integrity sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA== + +tsconfig-paths@^3.15.0, tsconfig-paths@^3.9.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== @@ -5193,6 +5745,11 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.1.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" @@ -5205,6 +5762,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-detect@^4.0.0, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -5267,6 +5829,11 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +ufo@^1.3.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" + integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== + uglify-js@^3.5.1: version "3.17.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" @@ -5360,6 +5927,36 @@ vh-check@^2.0.5: resolved "https://registry.yarnpkg.com/vh-check/-/vh-check-2.0.5.tgz#1b70610461e9776176f23d172daae3c4761aed09" integrity sha512-vHtIYWt9uLl2P2tLlatVpMwv9+ezuJCtMNjUVIpzd5Pa/dJXN8AtqkKmVRcNSlmXyCjkCkbMQX/Vs9axmdlfgg== +vite-jsconfig-paths@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/vite-jsconfig-paths/-/vite-jsconfig-paths-2.0.1.tgz#d66e36d67596dd8a8e4a6ed6e6db20debc50b45e" + integrity sha512-rabcTTfKs0MdAsQWcZjbIMo5fcp6jthZce7uFEPgVPgpSY+RNOwjzIJOPES6cB/GJZLSoLGfHM9kt5HNmJvp7A== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + recrawl-sync "^2.0.3" + tsconfig-paths "^3.9.0" + +vite-node@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.5.0.tgz#7f74dadfecb15bca016c5ce5ef85e5cc4b82abf2" + integrity sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw== + dependencies: + cac "^6.7.14" + debug "^4.3.4" + pathe "^1.1.1" + picocolors "^1.0.0" + vite "^5.0.0" + +vite-tsconfig-paths@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9" + integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + vite@^2.9.13: version "2.9.17" resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.17.tgz#6b770525e12fa2a2e3a0fa0d028d304f4f7dc7d4" @@ -5372,6 +5969,43 @@ vite@^2.9.13: optionalDependencies: fsevents "~2.3.2" +vite@^5.0.0: + version "5.2.9" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.9.tgz#cd9a356c6ff5f7456c09c5ce74068ffa8df743d9" + integrity sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw== + dependencies: + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +vitest@^1.3.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.5.0.tgz#6ebb396bd358650011a9c96c18fa614b668365c1" + integrity sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw== + dependencies: + "@vitest/expect" "1.5.0" + "@vitest/runner" "1.5.0" + "@vitest/snapshot" "1.5.0" + "@vitest/spy" "1.5.0" + "@vitest/utils" "1.5.0" + acorn-walk "^8.3.2" + chai "^4.3.10" + debug "^4.3.4" + execa "^8.0.1" + local-pkg "^0.5.0" + magic-string "^0.30.5" + pathe "^1.1.1" + picocolors "^1.0.0" + std-env "^3.5.0" + strip-literal "^2.0.0" + tinybench "^2.5.1" + tinypool "^0.8.3" + vite "^5.0.0" + vite-node "1.5.0" + why-is-node-running "^2.2.2" + vue-chartjs@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/vue-chartjs/-/vue-chartjs-5.3.0.tgz#59920a07d72f37a2375d495256e486b92813bf6e" @@ -5432,6 +6066,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + webpack-merge@^5.8.0: version "5.10.0" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" @@ -5441,6 +6080,11 @@ webpack-merge@^5.8.0: flat "^5.0.2" wildcard "^2.0.0" +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -5470,16 +6114,19 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" + integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + wildcard@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== -workerpool@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" - integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -5532,44 +6179,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@20.2.4: - version "20.2.4" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs-unparser@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" - integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== - dependencies: - camelcase "^6.0.0" - decamelize "^4.0.0" - flat "^5.0.2" - is-plain-obj "^2.1.0" - -yargs@16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^17.5.1: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" From f02b223639d67217e400392f44688cc20b055f23 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Tue, 21 May 2024 16:16:05 +0200 Subject: [PATCH 22/26] Prepare release v2.11.3 --- CHANGELOG.md | 16 ++++++++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d5d64508..c2e6ebfe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## [v2.11.3](https://github.com/traefik/traefik/tree/v2.11.3) (2024-05-17) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.2...v2.11.3) + +**Bug fixes:** +- **[server]** Remove deadlines for non-TLS connections ([#10615](https://github.com/traefik/traefik/pull/10615) by [rtribotte](https://github.com/rtribotte)) +- **[webui]** Display of Content Security Policy values getting out of screen ([#10710](https://github.com/traefik/traefik/pull/10710) by [brandonfl](https://github.com/brandonfl)) +- **[webui]** Fix provider icon size ([#10621](https://github.com/traefik/traefik/pull/10621) by [framebassman](https://github.com/framebassman)) + +**Documentation:** +- **[k8s/crd]** Fix migration/v2.md ([#10658](https://github.com/traefik/traefik/pull/10658) by [stemar94](https://github.com/stemar94)) +- **[k8s/gatewayapi]** Fix HTTPRoute use of backendRefs ([#10630](https://github.com/traefik/traefik/pull/10630) by [sakaru](https://github.com/sakaru)) +- **[k8s/gatewayapi]** Fix HTTPRoute path type ([#10629](https://github.com/traefik/traefik/pull/10629) by [sakaru](https://github.com/sakaru)) +- **[k8s]** Improve mirroring example on Kubernetes ([#10701](https://github.com/traefik/traefik/pull/10701) by [mloiseleur](https://github.com/mloiseleur)) +- Consistent entryPoints capitalization in CLI flag usage ([#10650](https://github.com/traefik/traefik/pull/10650) by [jnoordsij](https://github.com/jnoordsij)) +- Fix unfinished migration sentence for v2.11.2 ([#10633](https://github.com/traefik/traefik/pull/10633) by [kevinpollet](https://github.com/kevinpollet)) + ## [v2.11.2](https://github.com/traefik/traefik/tree/v2.11.2) (2024-04-11) [All Commits](https://github.com/traefik/traefik/compare/v2.11.1...v2.11.2) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index cc811ea47..241c83616 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.2 +# example new bugfix v2.11.3 CurrentRef = "v2.11" -PreviousRef = "v2.11.1" +PreviousRef = "v2.11.2" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.2" +FutureCurrentRefName = "v2.11.3" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 From 736f37cb583550bf62d98970ee289771d6b5c1c5 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Wed, 22 May 2024 15:08:04 +0200 Subject: [PATCH 23/26] Prepare release v3.0.1 --- CHANGELOG.md | 20 ++++++++++++++++++++ script/gcg/traefik-bugfix.toml | 10 +++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 201051444..f4d550ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## [v3.0.1](https://github.com/traefik/traefik/tree/v3.0.1) (2024-05-22) +[All Commits](https://github.com/traefik/traefik/compare/v3.0.0...v3.0.1) + +**Bug fixes:** +- **[k8s/ingress]** Fix rule syntax version for all internal routers ([#10689](https://github.com/traefik/traefik/pull/10689) by [HalloTschuess](https://github.com/HalloTschuess)) +- **[metrics,tracing]** Allow empty configuration for OpenTelemetry metrics and tracing ([#10729](https://github.com/traefik/traefik/pull/10729) by [rtribotte](https://github.com/rtribotte)) +- **[provider,tls]** Bump tscert dependency to 28a91b69a046 ([#10668](https://github.com/traefik/traefik/pull/10668) by [kevinpollet](https://github.com/kevinpollet)) +- **[rules,tcp]** Fix the rule syntax mechanism for TCP ([#10680](https://github.com/traefik/traefik/pull/10680) by [lbenguigui](https://github.com/lbenguigui)) +- **[tls,server]** Remove deadlines when handling PostgreSQL connections ([#10675](https://github.com/traefik/traefik/pull/10675) by [rtribotte](https://github.com/rtribotte)) +- **[webui]** Add support for IP White list ([#10740](https://github.com/traefik/traefik/pull/10740) by [davidbaptista](https://github.com/davidbaptista)) + +**Documentation:** +- **[http3]** Add link to the new http3 config in migration ([#10673](https://github.com/traefik/traefik/pull/10673) by [yyewolf](https://github.com/yyewolf)) +- **[logs]** Fix log.compress value ([#10716](https://github.com/traefik/traefik/pull/10716) by [mmatur](https://github.com/mmatur)) +- **[metrics]** Fix OTel documentation ([#10723](https://github.com/traefik/traefik/pull/10723) by [nmengin](https://github.com/nmengin)) +- **[middleware]** Fix doc consistency forwardauth ([#10724](https://github.com/traefik/traefik/pull/10724) by [mmatur](https://github.com/mmatur)) +- **[middleware]** Remove providers not supported in documentation ([#10725](https://github.com/traefik/traefik/pull/10725) by [mmatur](https://github.com/mmatur)) +- **[rules]** Fix typo in PathRegexp explanation ([#10719](https://github.com/traefik/traefik/pull/10719) by [BreadInvasion](https://github.com/BreadInvasion)) +- **[rules]** Fix router documentation example ([#10704](https://github.com/traefik/traefik/pull/10704) by [ldez](https://github.com/ldez)) + ## [v2.11.3](https://github.com/traefik/traefik/tree/v2.11.3) (2024-05-17) [All Commits](https://github.com/traefik/traefik/compare/v2.11.2...v2.11.3) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 241c83616..b6c09ed2b 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.3 -CurrentRef = "v2.11" -PreviousRef = "v2.11.2" -BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.3" +# example new bugfix v3.0.1 +CurrentRef = "v3.0" +PreviousRef = "v3.0.0" +BaseBranch = "v3.0" +FutureCurrentRefName = "v3.0.1" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 From 0e215f9b616afc004708693494ba8510495ad564 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Wed, 22 May 2024 17:20:04 +0200 Subject: [PATCH 24/26] Support invalid HTTPRoute status Co-authored-by: Romain --- integration/k8s_conformance_test.go | 2 - pkg/config/dynamic/http_config.go | 4 + pkg/config/dynamic/zz_generated.deepcopy.go | 5 + .../httproute/simple_with_bad_rule.yml | 2 +- pkg/provider/kubernetes/gateway/kubernetes.go | 382 +++++++++--------- .../kubernetes/gateway/kubernetes_test.go | 255 +++++++----- pkg/server/service/service.go | 8 + 7 files changed, 361 insertions(+), 297 deletions(-) diff --git a/integration/k8s_conformance_test.go b/integration/k8s_conformance_test.go index 01dd4856b..6c6d99ee1 100644 --- a/integration/k8s_conformance_test.go +++ b/integration/k8s_conformance_test.go @@ -205,7 +205,6 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { tests.GatewayInvalidTLSConfiguration.ShortName, tests.HTTPRouteHostnameIntersection.ShortName, tests.HTTPRouteListenerHostnameMatching.ShortName, - tests.HTTPRouteInvalidNonExistentBackendRef.ShortName, tests.HTTPRouteInvalidReferenceGrant.ShortName, tests.HTTPRouteInvalidCrossNamespaceParentRef.ShortName, tests.HTTPRouteInvalidParentRefNotMatchingSectionName.ShortName, @@ -213,7 +212,6 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { tests.HTTPRouteMatchingAcrossRoutes.ShortName, tests.HTTPRoutePartiallyInvalidViaInvalidReferenceGrant.ShortName, tests.HTTPRouteRedirectHostAndStatus.ShortName, - tests.HTTPRouteInvalidBackendRefUnknownKind.ShortName, tests.HTTPRoutePathMatchOrder.ShortName, tests.HTTPRouteHeaderMatching.ShortName, tests.HTTPRouteReferenceGrant.ShortName, diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 541f61b79..ecfd68c7e 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -128,6 +128,10 @@ type WeightedRoundRobin struct { type WRRService struct { Name string `json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty" export:"true"` Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" export:"true"` + + // Status defines an HTTP status code that should be returned when calling the service. + // This is required by the Gateway API implementation which expects specific HTTP status to be returned. + Status *int `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` } // SetDefaults Default values for a WRRService. diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 63b159382..05436088b 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -2187,6 +2187,11 @@ func (in *WRRService) DeepCopyInto(out *WRRService) { *out = new(int) **out = **in } + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(int) + **out = **in + } return } diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/simple_with_bad_rule.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/simple_with_bad_rule.yml index 0ff70ee1a..d1e0842a2 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/simple_with_bad_rule.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/simple_with_bad_rule.yml @@ -38,7 +38,7 @@ spec: rules: - matches: - path: - type: PathPrefix + type: Unsupported value: /bar backendRefs: - name: whoami diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index e4fee7308..ce3c12fe6 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -843,46 +843,52 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li return nil, nil } - var listenerConditions []metav1.Condition routeStatuses := map[ktypes.NamespacedName]gatev1.RouteParentStatus{} for _, route := range routes { + routeNsName := ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name} + parentRef, ok := shouldAttach(gateway, listener, route.Namespace, route.Spec.CommonRouteSpec) if !ok { + // TODO: to add an invalid HTTPRoute status when no parent is matching, + // we have to start the attachment evaluation from the route not from the listeners. + // This will fix the HTTPRouteInvalidParentRefNotMatchingSectionName test. continue } + routeConditions := []metav1.Condition{ + { + Type: string(gatev1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonAccepted), + }, + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteConditionResolvedRefs), + }, + } + hostnames := matchingHostnames(listener, route.Spec.Hostnames) if len(hostnames) == 0 && listener.Hostname != nil && *listener.Hostname != "" && len(route.Spec.Hostnames) > 0 { - // TODO update the corresponding route parent status + // TODO update the corresponding route parent status. // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute continue } hostRule, err := hostRule(hostnames) if err != nil { - listenerConditions = append(listenerConditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidRouteHostname", // TODO check the spec if a proper reason is introduced at some point - Message: fmt.Sprintf("Skipping HTTPRoute %s: invalid hostname: %v", route.Name, err), - }) + // TODO update the route status condition. continue } for _, routeRule := range route.Spec.Rules { rule, err := extractRule(routeRule, hostRule) if err != nil { - // update "ResolvedRefs" status true with "UnsupportedPathOrHeaderType" reason - listenerConditions = append(listenerConditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "UnsupportedPathOrHeaderType", // TODO check the spec if a proper reason is introduced at some point - Message: fmt.Sprintf("Skipping HTTPRoute %s: cannot generate rule: %v", route.Name, err), - }) + // TODO update the route status condition. continue } @@ -893,7 +899,7 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li } if listener.Protocol == gatev1.HTTPSProtocolType && listener.TLS != nil { - // TODO support let's encrypt + // TODO support let's encrypt. router.TLS = &dynamic.RouterTLSConfig{} } @@ -901,33 +907,13 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li routerName := route.Name + "-" + gateway.Name + "-" + ep routerKey, err := makeRouterKey(router.Rule, makeID(route.Namespace, routerName)) if err != nil { - // update "ResolvedRefs" status true with "DroppedRoutes" reason - listenerConditions = append(listenerConditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidRouterKey", // Should never happen - Message: fmt.Sprintf("Skipping HTTPRoute %s: cannot make router's key with rule %s: %v", route.Name, router.Rule, err), - }) - - // TODO update the RouteStatus condition / deduplicate conditions on listener + // TODO update the route status condition. continue } middlewares, err := p.loadMiddlewares(listener, route.Namespace, routerKey, routeRule.Filters) if err != nil { - // update "ResolvedRefs" status true with "InvalidFilters" reason - listenerConditions = append(listenerConditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidFilters", // TODO check the spec if a proper reason is introduced at some point - Message: fmt.Sprintf("Cannot load HTTPRoute filter %s/%s: %v", route.Namespace, route.Name, err), - }) - - // TODO update the RouteStatus condition / deduplicate conditions on listener + // TODO update the route status condition. continue } @@ -948,32 +934,35 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef) { router.Service = string(routeRule.BackendRefs[0].Name) } else { - wrrService, subServices, err := p.loadServices(client, route.Namespace, routeRule.BackendRefs) - if err != nil { - // update "ResolvedRefs" status true with "DroppedRoutes" reason - listenerConditions = append(listenerConditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidBackendRefs", // TODO check the spec if a proper reason is introduced at some point - Message: fmt.Sprintf("Cannot load HTTPRoute service %s/%s: %v", route.Namespace, route.Name, err), - }) + var wrr dynamic.WeightedRoundRobin + for _, backendRef := range routeRule.BackendRefs { + weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1))) - // TODO update the RouteStatus condition / deduplicate conditions on listener - continue - } - - for svcName, svc := range subServices { - if svc != nil { - conf.HTTP.Services[svcName] = svc + name, svc, errCondition := p.loadHTTPService(client, route, backendRef) + if errCondition != nil { + routeConditions = appendCondition(routeConditions, *errCondition) + wrr.Services = append(wrr.Services, dynamic.WRRService{ + Name: name, + Weight: weight, + Status: ptr.To(500), + }) + continue } + + if svc != nil { + conf.HTTP.Services[name] = svc + } + + wrr.Services = append(wrr.Services, dynamic.WRRService{ + Name: name, + Weight: weight, + }) } - serviceName := provider.Normalize(routerKey + "-wrr") - conf.HTTP.Services[serviceName] = wrrService + wrrName := provider.Normalize(routerKey + "-wrr") + conf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr} - router.Service = serviceName + router.Service = wrrName } rt := &router @@ -983,159 +972,85 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li conf.HTTP.Routers[routerKey] = rt } - routeStatuses[ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}] = gatev1.RouteParentStatus{ + routeStatuses[routeNsName] = gatev1.RouteParentStatus{ ParentRef: parentRef, ControllerName: controllerName, - Conditions: []metav1.Condition{ - { - Type: string(gatev1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonAccepted), - }, - { - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteConditionResolvedRefs), - }, - }, + Conditions: routeConditions, } } - return listenerConditions, routeStatuses + return nil, routeStatuses } -// loadServices is generating a WRR service, even when there is only one target. -func (p *Provider) loadServices(client Client, namespace string, backendRefs []gatev1.HTTPBackendRef) (*dynamic.Service, map[string]*dynamic.Service, error) { - services := map[string]*dynamic.Service{} - - wrrSvc := &dynamic.Service{ - Weighted: &dynamic.WeightedRoundRobin{ - Services: []dynamic.WRRService{}, - }, +// loadHTTPService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef. +// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc). +func (p *Provider) loadHTTPService(client Client, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, *metav1.Condition) { + group := groupCore + if backendRef.Group != nil && *backendRef.Group != "" { + group = string(*backendRef.Group) } - for _, backendRef := range backendRefs { - if backendRef.Group == nil || backendRef.Kind == nil { - // Should not happen as this is validated by kubernetes - continue - } + kind := ptr.Deref(backendRef.Kind, "Service") + namespace := ptr.Deref(backendRef.Namespace, gatev1.Namespace(route.Namespace)) + namespaceStr := string(namespace) + serviceName := provider.Normalize(makeID(namespaceStr, string(backendRef.Name))) - if isInternalService(backendRef.BackendRef) { - return nil, nil, fmt.Errorf("traefik internal service %s is not allowed in a WRR loadbalancer", backendRef.BackendRef.Name) - } - - weight := int(ptr.Deref(backendRef.Weight, 1)) - - if *backendRef.Group != "" && *backendRef.Group != groupCore && *backendRef.Kind != "Service" { - if backendRef.Namespace != nil && string(*backendRef.Namespace) != namespace { - // TODO: support backend reference grant. - return nil, nil, fmt.Errorf("unsupported HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) + if group != groupCore || kind != "Service" { + // TODO support cross namespace through ReferencePolicy. + if namespaceStr != route.Namespace { + return serviceName, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s namespace not allowed", group, kind, namespace, backendRef.Name), } - - name, service, err := p.loadHTTPBackendRef(namespace, backendRef) - if err != nil { - return nil, nil, fmt.Errorf("unable to load HTTPBackendRef %s/%s/%s: %w", *backendRef.Group, *backendRef.Kind, backendRef.Name, err) - } - - services[name] = service - wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.WRRService{Name: name, Weight: &weight}) - continue } - lb := &dynamic.ServersLoadBalancer{} - lb.SetDefaults() - - svc := dynamic.Service{LoadBalancer: lb} - - // TODO support cross namespace through ReferencePolicy - service, exists, err := client.GetService(namespace, string(backendRef.Name)) + name, service, err := p.loadHTTPBackendRef(namespaceStr, backendRef) if err != nil { - return nil, nil, err - } - - if !exists { - return nil, nil, errors.New("service not found") - } - - if len(service.Spec.Ports) > 1 && backendRef.Port == nil { - // If the port is unspecified and the backend is a Service - // object consisting of multiple port definitions, the route - // must be dropped from the Gateway. The controller should - // raise the "ResolvedRefs" condition on the Gateway with the - // "DroppedRoutes" reason. The gateway status for this route - // should be updated with a condition that describes the error - // more specifically. - log.Error().Msg("A multiple ports Kubernetes Service cannot be used if unspecified backendRef.Port") - continue - } - - var portSpec corev1.ServicePort - var match bool - - for _, p := range service.Spec.Ports { - if backendRef.Port == nil || p.Port == int32(*backendRef.Port) { - portSpec = p - match = true - break + return serviceName, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonInvalidKind), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), } } - if !match { - return nil, nil, errors.New("service port not found") - } - - endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, string(backendRef.Name)) - if endpointsErr != nil { - return nil, nil, endpointsErr - } - - if !endpointsExists { - return nil, nil, errors.New("endpoints not found") - } - - if len(endpoints.Subsets) == 0 { - return nil, nil, errors.New("subset not found") - } - - var port int32 - var portStr string - for _, subset := range endpoints.Subsets { - for _, p := range subset.Ports { - if portSpec.Name == p.Name { - port = p.Port - break - } - } - - if port == 0 { - return nil, nil, errors.New("cannot define a port") - } - - protocol := getProtocol(portSpec) - - portStr = strconv.FormatInt(int64(port), 10) - for _, addr := range subset.Addresses { - svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(addr.IP, portStr)), - }) - } - } - - serviceName := provider.Normalize(makeID(service.Namespace, service.Name) + "-" + portStr) - services[serviceName] = &svc - - wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.WRRService{Name: serviceName, Weight: &weight}) + return name, service, nil } - if len(wrrSvc.Weighted.Services) == 0 { - return nil, nil, errors.New("no service has been created") + port := ptr.Deref(backendRef.Port, gatev1.PortNumber(0)) + if port == 0 { + return serviceName, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonUnsupportedProtocol), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s port is required", group, kind, namespace, backendRef.Name), + } } - return wrrSvc, services, nil + portStr := strconv.FormatInt(int64(port), 10) + serviceName = provider.Normalize(serviceName + "-" + portStr) + + lb, err := loadHTTPServers(client, namespaceStr, backendRef) + if err != nil { + return serviceName, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonBackendNotFound), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), + } + } + + return serviceName, &dynamic.Service{LoadBalancer: lb}, nil } func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, error) { @@ -1224,6 +1139,72 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe return filterFunc(string(extensionRef.Name), namespace) } +// TODO support cross namespace through ReferencePolicy. +func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) { + service, exists, err := client.GetService(namespace, string(backendRef.Name)) + if err != nil { + return nil, fmt.Errorf("getting service: %w", err) + } + if !exists { + return nil, errors.New("service not found") + } + + var portSpec corev1.ServicePort + var match bool + + for _, p := range service.Spec.Ports { + if backendRef.Port == nil || p.Port == int32(*backendRef.Port) { + portSpec = p + match = true + break + } + } + if !match { + return nil, errors.New("service port not found") + } + + endpoints, endpointsExists, err := client.GetEndpoints(namespace, string(backendRef.Name)) + if err != nil { + return nil, fmt.Errorf("getting endpoints: %w", err) + } + if !endpointsExists { + return nil, errors.New("endpoints not found") + } + + if len(endpoints.Subsets) == 0 { + return nil, errors.New("subset not found") + } + + lb := &dynamic.ServersLoadBalancer{} + lb.SetDefaults() + + var port int32 + var portStr string + for _, subset := range endpoints.Subsets { + for _, p := range subset.Ports { + if portSpec.Name == p.Name { + port = p.Port + break + } + } + + if port == 0 { + return nil, errors.New("cannot define a port") + } + + protocol := getProtocol(portSpec) + + portStr = strconv.FormatInt(int64(port), 10) + for _, addr := range subset.Addresses { + lb.Servers = append(lb.Servers, dynamic.Server{ + URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(addr.IP, portStr)), + }) + } + } + + return lb, nil +} + // loadTCPServices is generating a WRR service, even when there is only one target. func loadTCPServices(client Client, namespace string, backendRefs []gatev1.BackendRef) (*dynamic.TCPService, map[string]*dynamic.TCPService, error) { services := map[string]*dynamic.TCPService{} @@ -2325,3 +2306,14 @@ func pointerEquals[T comparable](p1, p2 *T) bool { return val1 == val2 } + +func appendCondition(conditions []metav1.Condition, condition metav1.Condition) []metav1.Condition { + res := []metav1.Condition{condition} + for _, c := range conditions { + if c.Type != condition.Type { + res = append(res, c) + } + } + + return res +} diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index c428828b2..371ea2251 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -196,9 +196,28 @@ func TestLoadHTTPRoutes(t *testing.T) { ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, + Routers: map[string]*dynamic.Router{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-9000", + Weight: ptr.To(1), + Status: ptr.To(500), + }, + }, + }, + }, + }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, TLS: &dynamic.TLSConfiguration{}, @@ -586,7 +605,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -680,7 +699,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -749,7 +768,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -809,7 +828,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -869,7 +888,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -935,7 +954,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -945,7 +964,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami2-8080", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1021,11 +1040,11 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, { Name: "default-whoami2-8080", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1113,7 +1132,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1123,7 +1142,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1204,7 +1223,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1214,7 +1233,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1289,7 +1308,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1299,7 +1318,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1359,7 +1378,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1425,7 +1444,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1435,7 +1454,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "bar-whoami-bar-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1511,7 +1530,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "bar-whoami-bar-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1580,7 +1599,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1649,7 +1668,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1717,7 +1736,7 @@ func TestLoadHTTPRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1826,7 +1845,7 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { Services: []dynamic.WRRService{ { Name: "whoami", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1875,7 +1894,7 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { Services: []dynamic.WRRService{ { Name: "whoami", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -1911,9 +1930,28 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, + Routers: map[string]*dynamic.Router{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami", + Weight: ptr.To(1), + Status: ptr.To(500), + }, + }, + }, + }, + }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, TLS: &dynamic.TLSConfiguration{}, @@ -1942,9 +1980,28 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, + Routers: map[string]*dynamic.Router{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami", + Weight: ptr.To(1), + Status: ptr.To(500), + }, + }, + }, + }, + }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, TLS: &dynamic.TLSConfiguration{}, @@ -1989,11 +2046,11 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { Services: []dynamic.WRRService{ { Name: "service@file", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2103,7 +2160,7 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2170,7 +2227,7 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2503,7 +2560,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2567,7 +2624,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2577,7 +2634,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-10000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2647,11 +2704,11 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-0", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, { Name: "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-1", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2661,7 +2718,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2671,7 +2728,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-10000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2739,11 +2796,11 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "service@file", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2799,7 +2856,7 @@ func TestLoadTCPRoutes(t *testing.T) { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{{ Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }}, }, }, @@ -2863,7 +2920,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2925,7 +2982,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -2935,7 +2992,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3003,7 +3060,7 @@ func TestLoadTCPRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3317,7 +3374,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3385,7 +3442,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3444,7 +3501,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3511,7 +3568,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3521,7 +3578,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-10000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3599,11 +3656,11 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "service@file", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3671,7 +3728,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3730,7 +3787,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3789,7 +3846,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3848,7 +3905,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3907,7 +3964,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3975,7 +4032,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -3985,7 +4042,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4056,7 +4113,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4115,11 +4172,11 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-tls-app-my-gateway-tcp-1-673acf455cb2dab0b43a-wrr-0", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, { Name: "default-tls-app-my-gateway-tcp-1-673acf455cb2dab0b43a-wrr-1", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4129,7 +4186,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4139,7 +4196,7 @@ func TestLoadTLSRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-10000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4374,7 +4431,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4384,7 +4441,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4394,7 +4451,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4437,7 +4494,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4447,7 +4504,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4559,7 +4616,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4569,7 +4626,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4579,7 +4636,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4622,7 +4679,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4632,7 +4689,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4729,7 +4786,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4739,7 +4796,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4749,7 +4806,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4783,7 +4840,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4793,7 +4850,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4837,7 +4894,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4847,7 +4904,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4889,7 +4946,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "bar-whoami-bar-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4899,7 +4956,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "bar-whoami-bar-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4979,7 +5036,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4989,7 +5046,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -4999,7 +5056,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "bar-whoamitcp-bar-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -5046,7 +5103,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "bar-whoami-bar-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -5056,7 +5113,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "bar-whoami-bar-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -5114,7 +5171,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -5124,7 +5181,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.TCPWRRService{ { Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -5167,7 +5224,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -5177,7 +5234,7 @@ func TestLoadMixedRoutes(t *testing.T) { Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }, }, }, @@ -5387,7 +5444,7 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{{ Name: "default-whoamitcp-9000", - Weight: func(i int) *int { return &i }(1), + Weight: ptr.To(1), }}, }, }, diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index f44141f84..2a41836e3 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -222,6 +222,14 @@ func (m *Manager) getWRRServiceHandler(ctx context.Context, serviceName string, balancer := wrr.New(config.Sticky, config.HealthCheck != nil) for _, service := range shuffle(config.Services, m.rand) { + if service.Status != nil { + serviceHandler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(*service.Status) + }) + balancer.Add(service.Name, serviceHandler, service.Weight) + continue + } + serviceHandler, err := m.BuildHTTP(ctx, service.Name) if err != nil { return nil, err From 6e61fe0de1b0e02b3a664f72d5a856e165c57ec7 Mon Sep 17 00:00:00 2001 From: Dimitris Mavrommatis <75627010+dmavrommatis@users.noreply.github.com> Date: Thu, 23 May 2024 20:08:03 +0200 Subject: [PATCH 25/26] Support RegularExpression for path matching --- .../routing/providers/kubernetes-gateway.md | 2 +- .../fixtures/httproute/with_several_rules.yml | 11 +++++++++++ pkg/provider/kubernetes/gateway/kubernetes.go | 2 ++ .../kubernetes/gateway/kubernetes_test.go | 16 ++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/content/routing/providers/kubernetes-gateway.md b/docs/content/routing/providers/kubernetes-gateway.md index 6a5c52c2d..920a6ccf6 100644 --- a/docs/content/routing/providers/kubernetes-gateway.md +++ b/docs/content/routing/providers/kubernetes-gateway.md @@ -273,7 +273,7 @@ Kubernetes cluster before creating `HTTPRoute` objects. | [6] | `rules` | A list of HTTP matchers, filters and actions. | | [7] | `matches` | Conditions used for matching the rule against incoming HTTP requests. Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied. | | [8] | `path` | An HTTP request path matcher. If this field is not specified, a default prefix match on the "/" path is provided. | -| [9] | `type` | Type of match against the path Value (supported types: `Exact`, `PathPrefix`). | +| [9] | `type` | Type of match against the path Value (supported types: `Exact`, `PathPrefix`, and `RegularExpression`). | | [10] | `value` | The value of the HTTP path to match against. | | [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. | | [12] | `name` | Name of the HTTP header to be matched. | diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/with_several_rules.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/with_several_rules.yml index 3dc9b9ecf..f1885ec6f 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/with_several_rules.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/with_several_rules.yml @@ -68,3 +68,14 @@ spec: weight: 1 kind: Service group: "" + + - matches: + - path: + type: RegularExpression + value: "^/buzz/[0-9]+$" + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index ce3c12fe6..2aa5c8df8 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -1894,6 +1894,8 @@ func extractRule(routeRule gatev1.HTTPRouteRule, hostRule string) (string, error matchRules = append(matchRules, fmt.Sprintf("Path(`%s`)", *match.Path.Value)) case gatev1.PathMatchPathPrefix: matchRules = append(matchRules, buildPathMatchPathPrefixRule(*match.Path.Value)) + case gatev1.PathMatchRegularExpression: + matchRules = append(matchRules, fmt.Sprintf("PathRegexp(`%s`)", *match.Path.Value)) default: return "", fmt.Errorf("unsupported path match type %s", *match.Path.Type) } diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 371ea2251..d83ea3e38 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -1300,6 +1300,12 @@ func TestLoadHTTPRoutes(t *testing.T) { Rule: "Host(`foo.com`) && Path(`/bar`) && Header(`my-header`,`bar`)", RuleSyntax: "v3", }, + "default-http-app-1-my-gateway-web-d23f7039bc8036fb918c": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-d23f7039bc8036fb918c-wrr", + Rule: "Host(`foo.com`) && PathRegexp(`^/buzz/[0-9]+$`)", + RuleSyntax: "v3", + }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ @@ -1323,6 +1329,16 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, + "default-http-app-1-my-gateway-web-d23f7039bc8036fb918c-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-80", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, "default-whoami-80": { LoadBalancer: &dynamic.ServersLoadBalancer{ Servers: []dynamic.Server{ From e9bd2b45ac2bb1983fdee27d4efd76c450dfa338 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Tue, 28 May 2024 14:30:04 +0200 Subject: [PATCH 26/26] Fix route attachments to gateways Co-authored-by: Romain --- integration/k8s_conformance_test.go | 9 - pkg/provider/kubernetes/gateway/client.go | 268 +-- .../kubernetes/gateway/client_test.go | 36 +- .../httproute/simple_with_bad_rule.yml | 46 - .../tlsroute/with_invalid_SNI_matching.yml | 49 - pkg/provider/kubernetes/gateway/httproute.go | 575 ++++++ .../kubernetes/gateway/httproute_test.go | 281 +++ pkg/provider/kubernetes/gateway/kubernetes.go | 1632 +++-------------- .../kubernetes/gateway/kubernetes_test.go | 1273 ++++--------- pkg/provider/kubernetes/gateway/tcproute.go | 296 +++ pkg/provider/kubernetes/gateway/tlsroute.go | 210 +++ .../kubernetes/gateway/tlsroute_test.go | 66 + 12 files changed, 2243 insertions(+), 2498 deletions(-) delete mode 100644 pkg/provider/kubernetes/gateway/fixtures/httproute/simple_with_bad_rule.yml delete mode 100644 pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_invalid_SNI_matching.yml create mode 100644 pkg/provider/kubernetes/gateway/httproute.go create mode 100644 pkg/provider/kubernetes/gateway/httproute_test.go create mode 100644 pkg/provider/kubernetes/gateway/tcproute.go create mode 100644 pkg/provider/kubernetes/gateway/tlsroute.go create mode 100644 pkg/provider/kubernetes/gateway/tlsroute_test.go diff --git a/integration/k8s_conformance_test.go b/integration/k8s_conformance_test.go index 6c6d99ee1..3ba562b2c 100644 --- a/integration/k8s_conformance_test.go +++ b/integration/k8s_conformance_test.go @@ -199,19 +199,10 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { RunTest: *k8sConformanceRunTest, // Until the feature are all supported, following tests are skipped. SkipTests: []string{ - tests.GatewayClassObservedGenerationBump.ShortName, - tests.GatewayWithAttachedRoutes.ShortName, - tests.GatewayModifyListeners.ShortName, - tests.GatewayInvalidTLSConfiguration.ShortName, - tests.HTTPRouteHostnameIntersection.ShortName, tests.HTTPRouteListenerHostnameMatching.ShortName, - tests.HTTPRouteInvalidReferenceGrant.ShortName, tests.HTTPRouteInvalidCrossNamespaceParentRef.ShortName, - tests.HTTPRouteInvalidParentRefNotMatchingSectionName.ShortName, - tests.HTTPRouteInvalidCrossNamespaceBackendRef.ShortName, tests.HTTPRouteMatchingAcrossRoutes.ShortName, tests.HTTPRoutePartiallyInvalidViaInvalidReferenceGrant.ShortName, - tests.HTTPRouteRedirectHostAndStatus.ShortName, tests.HTTPRoutePathMatchOrder.ShortName, tests.HTTPRouteHeaderMatching.ShortName, tests.HTTPRouteReferenceGrant.ShortName, diff --git a/pkg/provider/kubernetes/gateway/client.go b/pkg/provider/kubernetes/gateway/client.go index 06809147c..7c2756886 100644 --- a/pkg/provider/kubernetes/gateway/client.go +++ b/pkg/provider/kubernetes/gateway/client.go @@ -32,11 +32,11 @@ type resourceEventHandler struct { ev chan<- interface{} } -func (reh *resourceEventHandler) OnAdd(obj interface{}, isInInitialList bool) { +func (reh *resourceEventHandler) OnAdd(obj interface{}, _ bool) { eventHandlerFunc(reh.ev, obj) } -func (reh *resourceEventHandler) OnUpdate(oldObj, newObj interface{}) { +func (reh *resourceEventHandler) OnUpdate(_, newObj interface{}) { eventHandlerFunc(reh.ev, newObj) } @@ -49,19 +49,21 @@ func (reh *resourceEventHandler) OnDelete(obj interface{}) { // The stores can then be accessed via the Get* functions. type Client interface { WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) - GetGatewayClasses() ([]*gatev1.GatewayClass, error) UpdateGatewayStatus(gateway *gatev1.Gateway, gatewayStatus gatev1.GatewayStatus) error UpdateGatewayClassStatus(gatewayClass *gatev1.GatewayClass, condition metav1.Condition) error - UpdateHTTPRouteStatus(ctx context.Context, gateway *gatev1.Gateway, nsName ktypes.NamespacedName, status gatev1.HTTPRouteStatus) error - GetGateways() []*gatev1.Gateway - GetHTTPRoutes(namespaces []string) ([]*gatev1.HTTPRoute, error) - GetTCPRoutes(namespaces []string) ([]*gatev1alpha2.TCPRoute, error) - GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRoute, error) - GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) + UpdateHTTPRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1.HTTPRouteStatus) error + UpdateTCPRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1alpha2.TCPRouteStatus) error + UpdateTLSRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1alpha2.TLSRouteStatus) error + ListGatewayClasses() ([]*gatev1.GatewayClass, error) + ListGateways() []*gatev1.Gateway + ListHTTPRoutes() ([]*gatev1.HTTPRoute, error) + ListTCPRoutes() ([]*gatev1alpha2.TCPRoute, error) + ListTLSRoutes() ([]*gatev1alpha2.TLSRoute, error) + ListNamespaces(selector labels.Selector) ([]string, error) + ListReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) GetService(namespace, name string) (*corev1.Service, bool, error) GetSecret(namespace, name string) (*corev1.Secret, bool, error) GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) - GetNamespaces(selector labels.Selector) ([]string, error) } type clientWrapper struct { @@ -280,7 +282,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< return eventCh, nil } -func (c *clientWrapper) GetNamespaces(selector labels.Selector) ([]string, error) { +func (c *clientWrapper) ListNamespaces(selector labels.Selector) ([]string, error) { ns, err := c.factoryNamespace.Core().V1().Namespaces().Lister().List(selector) if err != nil { return nil, err @@ -297,22 +299,12 @@ func (c *clientWrapper) GetNamespaces(selector labels.Selector) ([]string, error return namespaces, nil } -func (c *clientWrapper) GetHTTPRoutes(namespaces []string) ([]*gatev1.HTTPRoute, error) { +func (c *clientWrapper) ListHTTPRoutes() ([]*gatev1.HTTPRoute, error) { var httpRoutes []*gatev1.HTTPRoute - for _, namespace := range namespaces { - if !c.isWatchedNamespace(namespace) { - log.Warn().Msgf("Failed to get HTTPRoutes: %q is not within watched namespaces", namespace) - continue - } - + for _, namespace := range c.watchedNamespaces { routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1().HTTPRoutes().Lister().HTTPRoutes(namespace).List(labels.Everything()) if err != nil { - return nil, err - } - - if len(routes) == 0 { - log.Debug().Msgf("No HTTPRoutes found in namespace %q", namespace) - continue + return nil, fmt.Errorf("listing HTTP routes in namespace %s", namespace) } httpRoutes = append(httpRoutes, routes...) @@ -321,53 +313,35 @@ func (c *clientWrapper) GetHTTPRoutes(namespaces []string) ([]*gatev1.HTTPRoute, return httpRoutes, nil } -func (c *clientWrapper) GetTCPRoutes(namespaces []string) ([]*gatev1alpha2.TCPRoute, error) { +func (c *clientWrapper) ListTCPRoutes() ([]*gatev1alpha2.TCPRoute, error) { var tcpRoutes []*gatev1alpha2.TCPRoute - for _, namespace := range namespaces { - if !c.isWatchedNamespace(namespace) { - log.Warn().Msgf("Failed to get TCPRoutes: %q is not within watched namespaces", namespace) - continue - } - + for _, namespace := range c.watchedNamespaces { routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1alpha2().TCPRoutes().Lister().TCPRoutes(namespace).List(labels.Everything()) if err != nil { - return nil, err - } - - if len(routes) == 0 { - log.Debug().Msgf("No TCPRoutes found in namespace %q", namespace) - continue + return nil, fmt.Errorf("listing TCP routes in namespace %s", namespace) } tcpRoutes = append(tcpRoutes, routes...) } + return tcpRoutes, nil } -func (c *clientWrapper) GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRoute, error) { +func (c *clientWrapper) ListTLSRoutes() ([]*gatev1alpha2.TLSRoute, error) { var tlsRoutes []*gatev1alpha2.TLSRoute - for _, namespace := range namespaces { - if !c.isWatchedNamespace(namespace) { - log.Warn().Msgf("Failed to get TLSRoutes: %q is not within watched namespaces", namespace) - continue - } - + for _, namespace := range c.watchedNamespaces { routes, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1alpha2().TLSRoutes().Lister().TLSRoutes(namespace).List(labels.Everything()) if err != nil { - return nil, err - } - - if len(routes) == 0 { - log.Debug().Msgf("No TLSRoutes found in namespace %q", namespace) - continue + return nil, fmt.Errorf("listing TLS routes in namespace %s", namespace) } tlsRoutes = append(tlsRoutes, routes...) } + return tlsRoutes, nil } -func (c *clientWrapper) GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) { +func (c *clientWrapper) ListReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) { if !c.isWatchedNamespace(namespace) { log.Warn().Msgf("Failed to get ReferenceGrants: %q is not within watched namespaces", namespace) @@ -382,7 +356,7 @@ func (c *clientWrapper) GetReferenceGrants(namespace string) ([]*gatev1beta1.Ref return referenceGrants, nil } -func (c *clientWrapper) GetGateways() []*gatev1.Gateway { +func (c *clientWrapper) ListGateways() []*gatev1.Gateway { var result []*gatev1.Gateway for ns, factory := range c.factoriesGateway { @@ -397,7 +371,7 @@ func (c *clientWrapper) GetGateways() []*gatev1.Gateway { return result } -func (c *clientWrapper) GetGatewayClasses() ([]*gatev1.GatewayClass, error) { +func (c *clientWrapper) ListGatewayClasses() ([]*gatev1.GatewayClass, error) { return c.factoryGatewayClass.Gateway().V1().GatewayClasses().Lister().List(labels.Everything()) } @@ -437,7 +411,7 @@ func (c *clientWrapper) UpdateGatewayStatus(gateway *gatev1.Gateway, gatewayStat return fmt.Errorf("cannot update Gateway status %s/%s: namespace is not within watched namespaces", gateway.Namespace, gateway.Name) } - if statusEquals(gateway.Status, gatewayStatus) { + if gatewayStatusEquals(gateway.Status, gatewayStatus) { return nil } @@ -455,89 +429,106 @@ func (c *clientWrapper) UpdateGatewayStatus(gateway *gatev1.Gateway, gatewayStat return nil } -func (c *clientWrapper) UpdateHTTPRouteStatus(ctx context.Context, gateway *gatev1.Gateway, nsName ktypes.NamespacedName, status gatev1.HTTPRouteStatus) error { - if !c.isWatchedNamespace(nsName.Namespace) { - return fmt.Errorf("updating HTTPRoute status %s/%s: namespace is not within watched namespaces", nsName.Namespace, nsName.Name) +func (c *clientWrapper) UpdateHTTPRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1.HTTPRouteStatus) error { + if !c.isWatchedNamespace(route.Namespace) { + return fmt.Errorf("updating HTTPRoute status %s/%s: namespace is not within watched namespaces", route.Namespace, route.Name) } - route, err := c.factoriesGateway[c.lookupNamespace(nsName.Namespace)].Gateway().V1().HTTPRoutes().Lister().HTTPRoutes(nsName.Namespace).Get(nsName.Name) + currentRoute, err := c.factoriesGateway[c.lookupNamespace(route.Namespace)].Gateway().V1().HTTPRoutes().Lister().HTTPRoutes(route.Namespace).Get(route.Name) if err != nil { - return fmt.Errorf("getting HTTPRoute %s/%s: %w", nsName.Namespace, nsName.Name, err) + return fmt.Errorf("getting HTTPRoute %s/%s: %w", route.Namespace, route.Name, err) } - var statuses []gatev1.RouteParentStatus - for _, status := range route.Status.Parents { - if status.ControllerName != controllerName { - statuses = append(statuses, status) - continue - } - if status.ParentRef.Namespace != nil && string(*status.ParentRef.Namespace) != gateway.Namespace { - statuses = append(statuses, status) - continue - } - if string(status.ParentRef.Name) != gateway.Name { - statuses = append(statuses, status) + // TODO: keep statuses for gateways managed by other Traefik instances. + var parentStatuses []gatev1.RouteParentStatus + for _, currentParentStatus := range currentRoute.Status.Parents { + if currentParentStatus.ControllerName != controllerName { + parentStatuses = append(parentStatuses, currentParentStatus) continue } } - statuses = append(statuses, status.Parents...) - route = route.DeepCopy() - route.Status = gatev1.HTTPRouteStatus{ + parentStatuses = append(parentStatuses, status.Parents...) + + currentRoute = currentRoute.DeepCopy() + currentRoute.Status = gatev1.HTTPRouteStatus{ RouteStatus: gatev1.RouteStatus{ - Parents: statuses, + Parents: parentStatuses, }, } - if _, err := c.csGateway.GatewayV1().HTTPRoutes(nsName.Namespace).UpdateStatus(ctx, route, metav1.UpdateOptions{}); err != nil { - return fmt.Errorf("updating HTTPRoute %s/%s status: %w", nsName.Namespace, nsName.Name, err) + if _, err := c.csGateway.GatewayV1().HTTPRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("updating HTTPRoute %s/%s status: %w", route.Namespace, route.Name, err) } return nil } -func statusEquals(oldStatus, newStatus gatev1.GatewayStatus) bool { - if len(oldStatus.Listeners) != len(newStatus.Listeners) { - return false +func (c *clientWrapper) UpdateTCPRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1alpha2.TCPRouteStatus) error { + if !c.isWatchedNamespace(route.Namespace) { + return fmt.Errorf("updating TCPRoute status %s/%s: namespace is not within watched namespaces", route.Namespace, route.Name) } - if !conditionsEquals(oldStatus.Conditions, newStatus.Conditions) { - return false + currentRoute, err := c.factoriesGateway[c.lookupNamespace(route.Namespace)].Gateway().V1alpha2().TCPRoutes().Lister().TCPRoutes(route.Namespace).Get(route.Name) + if err != nil { + return fmt.Errorf("getting TCPRoute %s/%s: %w", route.Namespace, route.Name, err) } - listenerMatches := 0 - for _, newListener := range newStatus.Listeners { - for _, oldListener := range oldStatus.Listeners { - if newListener.Name == oldListener.Name { - if !conditionsEquals(newListener.Conditions, oldListener.Conditions) { - return false - } - - listenerMatches++ - } + // TODO: keep statuses for gateways managed by other Traefik instances. + var parentStatuses []gatev1alpha2.RouteParentStatus + for _, currentParentStatus := range currentRoute.Status.Parents { + if currentParentStatus.ControllerName != controllerName { + parentStatuses = append(parentStatuses, currentParentStatus) + continue } } - return listenerMatches == len(oldStatus.Listeners) + parentStatuses = append(parentStatuses, status.Parents...) + + currentRoute = currentRoute.DeepCopy() + currentRoute.Status = gatev1alpha2.TCPRouteStatus{ + RouteStatus: gatev1.RouteStatus{ + Parents: parentStatuses, + }, + } + + if _, err := c.csGateway.GatewayV1alpha2().TCPRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("updating TCPRoute %s/%s status: %w", route.Namespace, route.Name, err) + } + return nil } -func conditionsEquals(conditionsA, conditionsB []metav1.Condition) bool { - if len(conditionsA) != len(conditionsB) { - return false +func (c *clientWrapper) UpdateTLSRouteStatus(ctx context.Context, route ktypes.NamespacedName, status gatev1alpha2.TLSRouteStatus) error { + if !c.isWatchedNamespace(route.Namespace) { + return fmt.Errorf("updating TLSRoute status %s/%s: namespace is not within watched namespaces", route.Namespace, route.Name) } - conditionMatches := 0 - for _, conditionA := range conditionsA { - for _, conditionB := range conditionsB { - if conditionA.Type == conditionB.Type { - if conditionA.Reason != conditionB.Reason || conditionA.Status != conditionB.Status || conditionA.Message != conditionB.Message || conditionA.ObservedGeneration != conditionB.ObservedGeneration { - return false - } - conditionMatches++ - } + currentRoute, err := c.factoriesGateway[c.lookupNamespace(route.Namespace)].Gateway().V1alpha2().TLSRoutes().Lister().TLSRoutes(route.Namespace).Get(route.Name) + if err != nil { + return fmt.Errorf("getting TLSRoute %s/%s: %w", route.Namespace, route.Name, err) + } + + // TODO: keep statuses for gateways managed by other Traefik instances. + var parentStatuses []gatev1alpha2.RouteParentStatus + for _, currentParentStatus := range currentRoute.Status.Parents { + if currentParentStatus.ControllerName != controllerName { + parentStatuses = append(parentStatuses, currentParentStatus) + continue } } - return conditionMatches == len(conditionsA) + parentStatuses = append(parentStatuses, status.Parents...) + + currentRoute = currentRoute.DeepCopy() + currentRoute.Status = gatev1alpha2.TLSRouteStatus{ + RouteStatus: gatev1.RouteStatus{ + Parents: parentStatuses, + }, + } + + if _, err := c.csGateway.GatewayV1alpha2().TLSRoutes(route.Namespace).UpdateStatus(ctx, currentRoute, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("updating TLSRoute %s/%s status: %w", route.Namespace, route.Name, err) + } + return nil } // GetService returns the named service from the given namespace. @@ -582,11 +573,21 @@ func (c *clientWrapper) GetSecret(namespace, name string) (*corev1.Secret, bool, // The distinction is necessary because we index all informers on the special // identifier iff all-namespaces are requested but receive specific namespace // identifiers from the Kubernetes API, so we have to bridge this gap. -func (c *clientWrapper) lookupNamespace(ns string) string { +func (c *clientWrapper) lookupNamespace(namespace string) string { if c.isNamespaceAll { return metav1.NamespaceAll } - return ns + return namespace +} + +// isWatchedNamespace checks to ensure that the namespace is being watched before we request +// it to ensure we don't panic by requesting an out-of-watch object. +func (c *clientWrapper) isWatchedNamespace(namespace string) bool { + if c.isNamespaceAll { + return true + } + + return slices.Contains(c.watchedNamespaces, namespace) } // eventHandlerFunc will pass the obj on to the events channel or drop it. @@ -608,12 +609,51 @@ func translateNotFoundError(err error) (bool, error) { return err == nil, err } -// isWatchedNamespace checks to ensure that the namespace is being watched before we request -// it to ensure we don't panic by requesting an out-of-watch object. -func (c *clientWrapper) isWatchedNamespace(ns string) bool { - if c.isNamespaceAll { - return true +func gatewayStatusEquals(statusA, statusB gatev1.GatewayStatus) bool { + if len(statusA.Listeners) != len(statusB.Listeners) { + return false } - return slices.Contains(c.watchedNamespaces, ns) + if !conditionsEquals(statusA.Conditions, statusB.Conditions) { + return false + } + + listenerMatches := 0 + for _, newListener := range statusB.Listeners { + for _, oldListener := range statusA.Listeners { + if newListener.Name == oldListener.Name { + if !conditionsEquals(newListener.Conditions, oldListener.Conditions) { + return false + } + + if newListener.AttachedRoutes != oldListener.AttachedRoutes { + return false + } + + listenerMatches++ + } + } + } + + return listenerMatches == len(statusA.Listeners) +} + +func conditionsEquals(conditionsA, conditionsB []metav1.Condition) bool { + if len(conditionsA) != len(conditionsB) { + return false + } + + conditionMatches := 0 + for _, conditionA := range conditionsA { + for _, conditionB := range conditionsB { + if conditionA.Type == conditionB.Type { + if conditionA.Reason != conditionB.Reason || conditionA.Status != conditionB.Status || conditionA.Message != conditionB.Message || conditionA.ObservedGeneration != conditionB.ObservedGeneration { + return false + } + conditionMatches++ + } + } + } + + return conditionMatches == len(conditionsA) } diff --git a/pkg/provider/kubernetes/gateway/client_test.go b/pkg/provider/kubernetes/gateway/client_test.go index d4bd4582b..6b371b6fc 100644 --- a/pkg/provider/kubernetes/gateway/client_test.go +++ b/pkg/provider/kubernetes/gateway/client_test.go @@ -8,7 +8,7 @@ import ( gatev1 "sigs.k8s.io/gateway-api/apis/v1" ) -func TestStatusEquals(t *testing.T) { +func Test_gatewayStatusEquals(t *testing.T) { testCases := []struct { desc string statusA gatev1.GatewayStatus @@ -230,13 +230,45 @@ func TestStatusEquals(t *testing.T) { }, expected: false, }, + { + desc: "Gateway listeners with same conditions but different number of attached routes", + statusA: gatev1.GatewayStatus{ + Listeners: []gatev1.ListenerStatus{ + { + Name: "foo", + AttachedRoutes: 1, + Conditions: []metav1.Condition{ + { + Type: "foobar", + Reason: "foobar", + }, + }, + }, + }, + }, + statusB: gatev1.GatewayStatus{ + Listeners: []gatev1.ListenerStatus{ + { + Name: "foo", + AttachedRoutes: 2, + Conditions: []metav1.Condition{ + { + Type: "foobar", + Reason: "foobar", + }, + }, + }, + }, + }, + expected: false, + }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { t.Parallel() - result := statusEquals(test.statusA, test.statusB) + result := gatewayStatusEquals(test.statusA, test.statusB) assert.Equal(t, test.expected, result) }) diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/simple_with_bad_rule.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/simple_with_bad_rule.yml deleted file mode 100644 index d1e0842a2..000000000 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/simple_with_bad_rule.yml +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: GatewayClass -apiVersion: gateway.networking.k8s.io/v1 -metadata: - name: my-gateway-class -spec: - controllerName: traefik.io/gateway-controller - ---- -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1 -metadata: - name: my-gateway - namespace: default -spec: - gatewayClassName: my-gateway-class - listeners: # Use GatewayClass defaults for listener definition. - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: Same - ---- -kind: HTTPRoute -apiVersion: gateway.networking.k8s.io/v1 -metadata: - name: http-app-1 - namespace: default -spec: - parentRefs: - - name: my-gateway - kind: Gateway - group: gateway.networking.k8s.io - hostnames: - - "foo.com" - rules: - - matches: - - path: - type: Unsupported - value: /bar - backendRefs: - - name: whoami - port: 80 - weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_invalid_SNI_matching.yml b/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_invalid_SNI_matching.yml deleted file mode 100644 index f1485032d..000000000 --- a/pkg/provider/kubernetes/gateway/fixtures/tlsroute/with_invalid_SNI_matching.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: GatewayClass -apiVersion: gateway.networking.k8s.io/v1 -metadata: - name: my-gateway-class -spec: - controllerName: traefik.io/gateway-controller - ---- -kind: Gateway -apiVersion: gateway.networking.k8s.io/v1 -metadata: - name: my-gateway - namespace: default -spec: - gatewayClassName: my-gateway-class - listeners: # Use GatewayClass defaults for listener definition. - - name: tls - protocol: TLS - port: 9001 - tls: - mode: Passthrough - allowedRoutes: - kinds: - - kind: TLSRoute - group: gateway.networking.k8s.io - namespaces: - from: Same - ---- -kind: TLSRoute -apiVersion: gateway.networking.k8s.io/v1alpha2 -metadata: - name: tls-app-1 - namespace: default -spec: - parentRefs: - - name: my-gateway - kind: Gateway - group: gateway.networking.k8s.io - hostnames: - - "*.foo.*.bar" - rules: - - backendRefs: - - name: whoamitcp - port: 9000 - weight: 1 - kind: Service - group: "" diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go new file mode 100644 index 000000000..3410764d9 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -0,0 +1,575 @@ +package gateway + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "regexp" + "strconv" + "strings" + + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/provider" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ktypes "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + gatev1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { + routes, err := client.ListHTTPRoutes() + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes") + return + } + + for _, route := range routes { + logger := log.Ctx(ctx).With(). + Str("http_route", route.Name). + Str("namespace", route.Namespace). + Logger() + + var parentStatuses []gatev1.RouteParentStatus + for _, parentRef := range route.Spec.ParentRefs { + parentStatus := &gatev1.RouteParentStatus{ + ParentRef: parentRef, + ControllerName: controllerName, + Conditions: []metav1.Condition{ + { + Type: string(gatev1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonAccepted), + }, + }, + } + + var attachedListeners bool + notAcceptedReason := gatev1.RouteReasonNoMatchingParent + for _, listener := range gatewayListeners { + if !matchListener(listener, route.Namespace, parentRef) { + continue + } + + if !allowRoute(listener, route.Namespace, kindHTTPRoute) { + notAcceptedReason = gatev1.RouteReasonNotAllowedByListeners + continue + } + + hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames) + if !ok { + notAcceptedReason = gatev1.RouteReasonNoMatchingListenerHostname + continue + } + + listener.Status.AttachedRoutes++ + + // TODO should we build the conf if the listener is not attached + // only consider the route attached if the listener is in an "attached" state. + if listener.Attached { + attachedListeners = true + } + resolveConditions := p.loadHTTPRoute(logger.WithContext(ctx), client, listener, route, hostnames, conf) + + // TODO: handle more accurately route conditions (in case of multiple listener matching). + for _, condition := range resolveConditions { + parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition) + } + } + + if !attachedListeners { + parentStatus.Conditions = []metav1.Condition{ + { + Type: string(gatev1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(notAcceptedReason), + }, + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonRefNotPermitted), + }, + } + } + + parentStatuses = append(parentStatuses, *parentStatus) + } + + status := gatev1.HTTPRouteStatus{ + RouteStatus: gatev1.RouteStatus{ + Parents: parentStatuses, + }, + } + if err := client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil { + logger.Error(). + Err(err). + Msg("Unable to update HTTPRoute status") + } + } +} + +func (p *Provider) loadHTTPRoute(ctx context.Context, client Client, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, conf *dynamic.Configuration) []metav1.Condition { + routeConditions := []metav1.Condition{ + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteConditionResolvedRefs), + }, + } + + hostRule := hostRule(hostnames) + + for _, routeRule := range route.Spec.Rules { + router := dynamic.Router{ + RuleSyntax: "v3", + Rule: routerRule(routeRule, hostRule), + EntryPoints: []string{listener.EPName}, + } + if listener.Protocol == gatev1.HTTPSProtocolType { + router.TLS = &dynamic.RouterTLSConfig{} + } + + // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. + routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName + routerKey := makeRouterKey(router.Rule, makeID(route.Namespace, routerName)) + + var wrr dynamic.WeightedRoundRobin + wrrName := provider.Normalize(routerKey + "-wrr") + + middlewares, err := p.loadMiddlewares(listener.Protocol, route.Namespace, routerKey, routeRule.Filters) + if err != nil { + log.Ctx(ctx).Error(). + Err(err). + Msg("Unable to load HTTPRoute filters") + + wrr.Services = append(wrr.Services, dynamic.WRRService{ + Name: "invalid-httproute-filter", + Status: ptr.To(500), + Weight: ptr.To(1), + }) + + conf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr} + router.Service = wrrName + } else { + for name, middleware := range middlewares { + // If the middleware config is nil in the return of the loadMiddlewares function, + // it means that we just need a reference to that middleware. + if middleware != nil { + conf.HTTP.Middlewares[name] = middleware + } + + router.Middlewares = append(router.Middlewares, name) + } + + // Traefik internal service can be used only if there is only one BackendRef service reference. + if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef) { + router.Service = string(routeRule.BackendRefs[0].Name) + } else { + for _, backendRef := range routeRule.BackendRefs { + name, svc, errCondition := p.loadHTTPService(client, route, backendRef) + weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1))) + if errCondition != nil { + routeConditions = appendCondition(routeConditions, *errCondition) + wrr.Services = append(wrr.Services, dynamic.WRRService{ + Name: name, + Status: ptr.To(500), + Weight: weight, + }) + continue + } + + if svc != nil { + conf.HTTP.Services[name] = svc + } + + wrr.Services = append(wrr.Services, dynamic.WRRService{ + Name: name, + Weight: weight, + }) + } + + conf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr} + router.Service = wrrName + } + } + + rt := &router + p.applyRouterTransform(ctx, rt, route) + + routerKey = provider.Normalize(routerKey) + conf.HTTP.Routers[routerKey] = rt + } + + return routeConditions +} + +// loadHTTPService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef. +// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc). +func (p *Provider) loadHTTPService(client Client, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, *metav1.Condition) { + group := groupCore + if backendRef.Group != nil && *backendRef.Group != "" { + group = string(*backendRef.Group) + } + + kind := ptr.Deref(backendRef.Kind, "Service") + namespace := ptr.Deref(backendRef.Namespace, gatev1.Namespace(route.Namespace)) + namespaceStr := string(namespace) + serviceName := provider.Normalize(makeID(namespaceStr, string(backendRef.Name))) + + // TODO support cross namespace through ReferenceGrant. + if namespaceStr != route.Namespace { + return serviceName, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s namespace not allowed", group, kind, namespace, backendRef.Name), + } + } + + if group != groupCore || kind != "Service" { + name, service, err := p.loadHTTPBackendRef(namespaceStr, backendRef) + if err != nil { + return serviceName, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonInvalidKind), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), + } + } + + return name, service, nil + } + + port := ptr.Deref(backendRef.Port, gatev1.PortNumber(0)) + if port == 0 { + return serviceName, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonUnsupportedProtocol), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s port is required", group, kind, namespace, backendRef.Name), + } + } + + portStr := strconv.FormatInt(int64(port), 10) + serviceName = provider.Normalize(serviceName + "-" + portStr) + + lb, err := loadHTTPServers(client, namespaceStr, backendRef) + if err != nil { + return serviceName, nil, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonBackendNotFound), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), + } + } + + return serviceName, &dynamic.Service{LoadBalancer: lb}, nil +} + +func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, error) { + // Support for cross-provider references (e.g: api@internal). + // This provides the same behavior as for IngressRoutes. + if *backendRef.Kind == "TraefikService" && strings.Contains(string(backendRef.Name), "@") { + return string(backendRef.Name), nil, nil + } + + backendFunc, ok := p.groupKindBackendFuncs[string(*backendRef.Group)][string(*backendRef.Kind)] + if !ok { + return "", nil, fmt.Errorf("unsupported HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) + } + if backendFunc == nil { + return "", nil, fmt.Errorf("undefined backendFunc for HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) + } + + return backendFunc(string(backendRef.Name), namespace) +} + +func (p *Provider) loadMiddlewares(listenerProtocol gatev1.ProtocolType, namespace, prefix string, filters []gatev1.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) { + middlewares := make(map[string]*dynamic.Middleware) + + for i, filter := range filters { + switch filter.Type { + case gatev1.HTTPRouteFilterRequestRedirect: + middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) + middlewares[middlewareName] = createRedirectRegexMiddleware(listenerProtocol, filter.RequestRedirect) + + case gatev1.HTTPRouteFilterRequestHeaderModifier: + middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) + middlewares[middlewareName] = createRequestHeaderModifier(filter.RequestHeaderModifier) + + case gatev1.HTTPRouteFilterExtensionRef: + name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef) + if err != nil { + return nil, fmt.Errorf("loading ExtensionRef filter %s: %w", filter.Type, err) + } + + middlewares[name] = middleware + + default: + // As per the spec: https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional + // In all cases where incompatible or unsupported filters are + // specified, implementations MUST add a warning condition to + // status. + return nil, fmt.Errorf("unsupported filter %s", filter.Type) + } + } + + return middlewares, nil +} + +func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRef *gatev1.LocalObjectReference) (string, *dynamic.Middleware, error) { + if extensionRef == nil { + return "", nil, errors.New("filter extension ref undefined") + } + + filterFunc, ok := p.groupKindFilterFuncs[string(extensionRef.Group)][string(extensionRef.Kind)] + if !ok { + return "", nil, fmt.Errorf("unsupported filter extension ref %s/%s/%s", extensionRef.Group, extensionRef.Kind, extensionRef.Name) + } + if filterFunc == nil { + return "", nil, fmt.Errorf("undefined filterFunc for filter extension ref %s/%s/%s", extensionRef.Group, extensionRef.Kind, extensionRef.Name) + } + + return filterFunc(string(extensionRef.Name), namespace) +} + +// TODO support cross namespace through ReferencePolicy. +func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) { + service, exists, err := client.GetService(namespace, string(backendRef.Name)) + if err != nil { + return nil, fmt.Errorf("getting service: %w", err) + } + if !exists { + return nil, errors.New("service not found") + } + + var portSpec corev1.ServicePort + var match bool + + for _, p := range service.Spec.Ports { + if backendRef.Port == nil || p.Port == int32(*backendRef.Port) { + portSpec = p + match = true + break + } + } + if !match { + return nil, errors.New("service port not found") + } + + endpoints, endpointsExists, err := client.GetEndpoints(namespace, string(backendRef.Name)) + if err != nil { + return nil, fmt.Errorf("getting endpoints: %w", err) + } + if !endpointsExists { + return nil, errors.New("endpoints not found") + } + + if len(endpoints.Subsets) == 0 { + return nil, errors.New("subset not found") + } + + lb := &dynamic.ServersLoadBalancer{} + lb.SetDefaults() + + var port int32 + var portStr string + for _, subset := range endpoints.Subsets { + for _, p := range subset.Ports { + if portSpec.Name == p.Name { + port = p.Port + break + } + } + + if port == 0 { + return nil, errors.New("cannot define a port") + } + + protocol := getProtocol(portSpec) + + portStr = strconv.FormatInt(int64(port), 10) + for _, addr := range subset.Addresses { + lb.Servers = append(lb.Servers, dynamic.Server{ + URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(addr.IP, portStr)), + }) + } + } + + return lb, nil +} + +func hostRule(hostnames []gatev1.Hostname) string { + var rules []string + + for _, hostname := range hostnames { + host := string(hostname) + + wildcard := strings.Count(host, "*") + if wildcard == 0 { + rules = append(rules, fmt.Sprintf("Host(`%s`)", host)) + continue + } + + host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-z0-9-\.]+\.`, 1) + rules = append(rules, fmt.Sprintf("HostRegexp(`^%s$`)", host)) + } + + switch len(rules) { + case 0: + return "" + case 1: + return rules[0] + default: + return fmt.Sprintf("(%s)", strings.Join(rules, " || ")) + } +} + +func routerRule(routeRule gatev1.HTTPRouteRule, hostRule string) string { + var rule string + var matchesRules []string + + for _, match := range routeRule.Matches { + path := ptr.Deref(match.Path, gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchPathPrefix), + Value: ptr.To("/"), + }) + pathType := ptr.Deref(path.Type, gatev1.PathMatchPathPrefix) + pathValue := ptr.Deref(path.Value, "/") + + var matchRules []string + switch pathType { + case gatev1.PathMatchExact: + matchRules = append(matchRules, fmt.Sprintf("Path(`%s`)", pathValue)) + case gatev1.PathMatchPathPrefix: + matchRules = append(matchRules, buildPathMatchPathPrefixRule(pathValue)) + case gatev1.PathMatchRegularExpression: + matchRules = append(matchRules, fmt.Sprintf("PathRegexp(`%s`)", pathValue)) + } + + matchRules = append(matchRules, headerRules(match.Headers)...) + matchesRules = append(matchesRules, strings.Join(matchRules, " && ")) + } + + // If no matches are specified, the default is a prefix + // path match on "/", which has the effect of matching every + // HTTP request. + if len(routeRule.Matches) == 0 { + matchesRules = append(matchesRules, "PathPrefix(`/`)") + } + + if hostRule != "" { + if len(matchesRules) == 0 { + return hostRule + } + rule += hostRule + " && " + } + + if len(matchesRules) == 1 { + return rule + matchesRules[0] + } + + if len(rule) == 0 { + return strings.Join(matchesRules, " || ") + } + + return rule + "(" + strings.Join(matchesRules, " || ") + ")" +} + +func headerRules(headers []gatev1.HTTPHeaderMatch) []string { + var headerRules []string + for _, header := range headers { + typ := ptr.Deref(header.Type, gatev1.HeaderMatchExact) + switch typ { + case gatev1.HeaderMatchExact: + headerRules = append(headerRules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value)) + case gatev1.HeaderMatchRegularExpression: + headerRules = append(headerRules, fmt.Sprintf("HeaderRegexp(`%s`,`%s`)", header.Name, header.Value)) + } + } + return headerRules +} + +func buildPathMatchPathPrefixRule(path string) string { + if path == "/" { + return "PathPrefix(`/`)" + } + + path = strings.TrimSuffix(path, "/") + return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", path) +} + +// createRequestHeaderModifier does not enforce/check the configuration, +// as the spec indicates that either the webhook or CEL (since v1.0 GA Release) should enforce that. +func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middleware { + sets := map[string]string{} + for _, header := range filter.Set { + sets[string(header.Name)] = header.Value + } + + adds := map[string]string{} + for _, header := range filter.Add { + adds[string(header.Name)] = header.Value + } + + return &dynamic.Middleware{ + RequestHeaderModifier: &dynamic.RequestHeaderModifier{ + Set: sets, + Add: adds, + Remove: filter.Remove, + }, + } +} + +func createRedirectRegexMiddleware(listenerProtocol gatev1.ProtocolType, filter *gatev1.HTTPRequestRedirectFilter) *dynamic.Middleware { + // The spec allows for an empty string in which case we should use the + // scheme of the request which in this case is the listener scheme. + filterScheme := ptr.Deref(filter.Scheme, strings.ToLower(string(listenerProtocol))) + statusCode := ptr.Deref(filter.StatusCode, http.StatusFound) + + port := "${port}" + if filter.Port != nil { + port = fmt.Sprintf(":%d", *filter.Port) + } + + hostname := "${hostname}" + if filter.Hostname != nil && *filter.Hostname != "" { + hostname = string(*filter.Hostname) + } + + return &dynamic.Middleware{ + RedirectRegex: &dynamic.RedirectRegex{ + Regex: `^[a-z]+:\/\/(?P.+@)?(?P\[[\w:\.]+\]|[\w\._-]+)(?P:\d+)?\/(?P.*)`, + Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port), + Permanent: statusCode == http.StatusMovedPermanently, + }, + } +} + +func getProtocol(portSpec corev1.ServicePort) string { + protocol := "http" + if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { + protocol = "https" + } + + return protocol +} diff --git a/pkg/provider/kubernetes/gateway/httproute_test.go b/pkg/provider/kubernetes/gateway/httproute_test.go new file mode 100644 index 000000000..2698acfdb --- /dev/null +++ b/pkg/provider/kubernetes/gateway/httproute_test.go @@ -0,0 +1,281 @@ +package gateway + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/utils/ptr" + gatev1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func Test_hostRule(t *testing.T) { + testCases := []struct { + desc string + hostnames []gatev1.Hostname + expectedRule string + expectErr bool + }{ + { + desc: "Empty rule and matches", + expectedRule: "", + }, + { + desc: "One Host", + hostnames: []gatev1.Hostname{ + "Foo", + }, + expectedRule: "Host(`Foo`)", + }, + { + desc: "Multiple Hosts", + hostnames: []gatev1.Hostname{ + "Foo", + "Bar", + "Bir", + }, + expectedRule: "(Host(`Foo`) || Host(`Bar`) || Host(`Bir`))", + }, + { + desc: "Several Host and wildcard", + hostnames: []gatev1.Hostname{ + "*.bar.foo", + "bar.foo", + "foo.foo", + }, + expectedRule: "(HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.foo$`) || Host(`bar.foo`) || Host(`foo.foo`))", + }, + { + desc: "Host with wildcard", + hostnames: []gatev1.Hostname{ + "*.bar.foo", + }, + expectedRule: "HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.foo$`)", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rule := hostRule(test.hostnames) + assert.Equal(t, test.expectedRule, rule) + }) + } +} + +func Test_routerRule(t *testing.T) { + testCases := []struct { + desc string + routeRule gatev1.HTTPRouteRule + hostRule string + expectedRule string + expectedError bool + }{ + { + desc: "Empty rule and matches", + expectedRule: "PathPrefix(`/`)", + }, + { + desc: "One Host rule without matches", + hostRule: "Host(`foo.com`)", + expectedRule: "Host(`foo.com`) && PathPrefix(`/`)", + }, + { + desc: "One HTTPRouteMatch with nil HTTPHeaderMatch", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: ptr.To(gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchPathPrefix), + Value: ptr.To("/"), + }), + Headers: nil, + }, + }, + }, + expectedRule: "PathPrefix(`/`)", + }, + { + desc: "One HTTPRouteMatch with nil HTTPHeaderMatch Type", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: ptr.To(gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchPathPrefix), + Value: ptr.To("/"), + }), + Headers: []gatev1.HTTPHeaderMatch{ + {Name: "foo", Value: "bar"}, + }, + }, + }, + }, + expectedRule: "PathPrefix(`/`) && Header(`foo`,`bar`)", + }, + { + desc: "One HTTPRouteMatch with nil HTTPPathMatch", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + {Path: nil}, + }, + }, + expectedRule: "PathPrefix(`/`)", + }, + { + desc: "One HTTPRouteMatch with nil HTTPPathMatch Type", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: &gatev1.HTTPPathMatch{ + Type: nil, + Value: ptr.To("/foo/"), + }, + }, + }, + }, + expectedRule: "(Path(`/foo`) || PathPrefix(`/foo/`))", + }, + { + desc: "One HTTPRouteMatch with nil HTTPPathMatch Values", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: nil, + }, + }, + }, + }, + expectedRule: "Path(`/`)", + }, + { + desc: "One Path in matches", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), + }, + }, + }, + }, + expectedRule: "Path(`/foo/`)", + }, + { + desc: "One Path in matches and another empty", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), + }, + }, + {}, + }, + }, + expectedRule: "Path(`/foo/`) || PathPrefix(`/`)", + }, + { + desc: "Path OR Header rules", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), + }, + }, + { + Headers: []gatev1.HTTPHeaderMatch{ + { + Type: ptr.To(gatev1.HeaderMatchExact), + Name: "my-header", + Value: "foo", + }, + }, + }, + }, + }, + expectedRule: "Path(`/foo/`) || PathPrefix(`/`) && Header(`my-header`,`foo`)", + }, + { + desc: "Path && Header rules", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), + }, + Headers: []gatev1.HTTPHeaderMatch{ + { + Type: ptr.To(gatev1.HeaderMatchExact), + Name: "my-header", + Value: "foo", + }, + }, + }, + }, + }, + expectedRule: "Path(`/foo/`) && Header(`my-header`,`foo`)", + }, + { + desc: "Host && Path && Header rules", + hostRule: "Host(`foo.com`)", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), + }, + Headers: []gatev1.HTTPHeaderMatch{ + { + Type: ptr.To(gatev1.HeaderMatchExact), + Name: "my-header", + Value: "foo", + }, + }, + }, + }, + }, + expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Header(`my-header`,`foo`)", + }, + { + desc: "Host && (Path || Header) rules", + hostRule: "Host(`foo.com`)", + routeRule: gatev1.HTTPRouteRule{ + Matches: []gatev1.HTTPRouteMatch{ + { + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), + }, + }, + { + Headers: []gatev1.HTTPHeaderMatch{ + { + Type: ptr.To(gatev1.HeaderMatchExact), + Name: "my-header", + Value: "foo", + }, + }, + }, + }, + }, + expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || PathPrefix(`/`) && Header(`my-header`,`foo`))", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rule := routerRule(test.routeRule, test.hostRule) + assert.Equal(t, test.expectedRule, rule) + }) + } +} diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 2aa5c8df8..e6cea12a1 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -5,10 +5,7 @@ import ( "crypto/sha256" "errors" "fmt" - "net" - "net/http" "os" - "regexp" "slices" "sort" "strconv" @@ -23,7 +20,6 @@ import ( "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/job" "github.com/traefik/traefik/v3/pkg/logs" - "github.com/traefik/traefik/v3/pkg/provider" traefikv1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" "github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s" "github.com/traefik/traefik/v3/pkg/safe" @@ -32,7 +28,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - ktypes "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" gatev1 "sigs.k8s.io/gateway-api/apis/v1" gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -104,6 +99,24 @@ type ExtensionBuilderRegistry interface { RegisterBackendFuncs(group, kind string, builderFunc BuildBackendFunc) } +type gatewayListener struct { + Name string + + Protocol gatev1.ProtocolType + TLS *gatev1.GatewayTLSConfig + Hostname *gatev1.Hostname + Status *gatev1.ListenerStatus + AllowedNamespaces []string + AllowedRouteKinds []string + + Attached bool + + GWName string + GWNamespace string + GWGeneration int64 + EPName string +} + // RegisterFilterFuncs registers an allowed Group, Kind, and builder for the Filter ExtensionRef objects. func (p *Provider) RegisterFilterFuncs(group, kind string, builderFunc BuildFilterFunc) { if p.groupKindFilterFuncs == nil { @@ -221,7 +234,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. // Note that event is the *first* event that came in during this throttling interval -- if we're hitting our throttle, we may have dropped events. // This is fine, because we don't treat different event types differently. // But if we do in the future, we'll need to track more information about the dropped events. - conf := p.loadConfigurationFromGateway(ctxLog, k8sClient) + conf := p.loadConfigurationFromGateways(ctxLog, k8sClient) confHash, err := hashstructure.Hash(conf, nil) switch { @@ -258,110 +271,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. } // TODO Handle errors and update resources statuses (gatewayClass, gateway). -func (p *Provider) loadConfigurationFromGateway(ctx context.Context, client Client) *dynamic.Configuration { - logger := log.Ctx(ctx) - - gatewayClassNames := map[string]struct{}{} - - gatewayClasses, err := client.GetGatewayClasses() - if err != nil { - logger.Error().Err(err).Msg("Cannot find GatewayClasses") - return &dynamic.Configuration{ - HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, - ServersTransports: map[string]*dynamic.ServersTransport{}, - }, - TCP: &dynamic.TCPConfiguration{ - Routers: map[string]*dynamic.TCPRouter{}, - Middlewares: map[string]*dynamic.TCPMiddleware{}, - Services: map[string]*dynamic.TCPService{}, - ServersTransports: map[string]*dynamic.TCPServersTransport{}, - }, - UDP: &dynamic.UDPConfiguration{ - Routers: map[string]*dynamic.UDPRouter{}, - Services: map[string]*dynamic.UDPService{}, - }, - TLS: &dynamic.TLSConfiguration{}, - } - } - - for _, gatewayClass := range gatewayClasses { - if gatewayClass.Spec.ControllerName == controllerName { - gatewayClassNames[gatewayClass.Name] = struct{}{} - - err := client.UpdateGatewayClassStatus(gatewayClass, metav1.Condition{ - Type: string(gatev1.GatewayClassConditionStatusAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: gatewayClass.Generation, - Reason: "Handled", - Message: "Handled by Traefik controller", - LastTransitionTime: metav1.Now(), - }) - if err != nil { - logger.Error().Err(err).Msgf("Failed to update %s condition", gatev1.GatewayClassConditionStatusAccepted) - } - } - } - - cfgs := map[string]*dynamic.Configuration{} - - // TODO check if we can only use the default filtering mechanism - for _, gateway := range client.GetGateways() { - logger := log.Ctx(ctx).With().Str("gateway", gateway.Name).Str("namespace", gateway.Namespace).Logger() - ctxLog := logger.WithContext(ctx) - - if _, ok := gatewayClassNames[string(gateway.Spec.GatewayClassName)]; !ok { - continue - } - - cfg, err := p.createGatewayConf(ctxLog, client, gateway) - if err != nil { - logger.Error().Err(err).Send() - continue - } - - cfgs[gateway.Name+gateway.Namespace] = cfg - } - - conf := provider.Merge(ctx, cfgs) - - conf.TLS = &dynamic.TLSConfiguration{} - - for _, cfg := range cfgs { - if conf.TLS == nil { - conf.TLS = &dynamic.TLSConfiguration{} - } - - conf.TLS.Certificates = append(conf.TLS.Certificates, cfg.TLS.Certificates...) - - for name, options := range cfg.TLS.Options { - if conf.TLS.Options == nil { - conf.TLS.Options = map[string]tls.Options{} - } - - conf.TLS.Options[name] = options - } - - for name, store := range cfg.TLS.Stores { - if conf.TLS.Stores == nil { - conf.TLS.Stores = map[string]tls.Store{} - } - - conf.TLS.Stores[name] = store - } - } - - return conf -} - -func (p *Provider) createGatewayConf(ctx context.Context, client Client, gateway *gatev1.Gateway) (*dynamic.Configuration, error) { - addresses, err := p.gatewayAddresses(client) - if err != nil { - return nil, fmt.Errorf("get Gateway status addresses: %w", err) - } - +func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Client) *dynamic.Configuration { conf := &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, @@ -382,74 +292,168 @@ func (p *Provider) createGatewayConf(ctx context.Context, client Client, gateway TLS: &dynamic.TLSConfiguration{}, } - tlsConfigs := make(map[string]*tls.CertAndStores) - - // GatewayReasonListenersNotValid is used when one or more - // Listeners have an invalid or unsupported configuration - // and cannot be configured on the Gateway. - listenerStatuses, httpRouteParentStatuses := p.fillGatewayConf(ctx, client, gateway, conf, tlsConfigs) - - if len(tlsConfigs) > 0 { - conf.TLS.Certificates = append(conf.TLS.Certificates, getTLSConfig(tlsConfigs)...) + addresses, err := p.gatewayAddresses(client) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("Unable to get Gateway status addresses") + return nil } - httpRouteStatuses := makeHTTPRouteStatuses(gateway.Namespace, httpRouteParentStatuses) - for nsName, status := range httpRouteStatuses { - if err := client.UpdateHTTPRouteStatus(ctx, gateway, nsName, status); err != nil { - log.Error(). + gatewayClasses, err := client.ListGatewayClasses() + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("Unable to list GatewayClasses") + return nil + } + + gatewayClassNames := map[string]struct{}{} + for _, gatewayClass := range gatewayClasses { + if gatewayClass.Spec.ControllerName != controllerName { + continue + } + + gatewayClassNames[gatewayClass.Name] = struct{}{} + + err := client.UpdateGatewayClassStatus(gatewayClass, metav1.Condition{ + Type: string(gatev1.GatewayClassConditionStatusAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: gatewayClass.Generation, + Reason: "Handled", + Message: "Handled by Traefik controller", + LastTransitionTime: metav1.Now(), + }) + if err != nil { + log.Ctx(ctx). + Error(). Err(err). - Str("namespace", nsName.Namespace). - Str("name", nsName.Name). - Msg("Unable to update HTTPRoute status") + Str("gateway_class", gatewayClass.Name). + Msg("Unable to update GatewayClass status") } } - gatewayStatus, errG := p.makeGatewayStatus(gateway, listenerStatuses, addresses) - if err = client.UpdateGatewayStatus(gateway, gatewayStatus); err != nil { - log.Error(). - Err(err). + gateways := client.ListGateways() + + var gatewayListeners []gatewayListener + for _, gateway := range gateways { + logger := log.Ctx(ctx).With(). + Str("gateway", gateway.Name). Str("namespace", gateway.Namespace). - Str("name", gateway.Name). - Msg("Unable to update Gateway status") - } - if errG != nil { - return nil, fmt.Errorf("creating gateway status: %w", errG) + Logger() + + if _, ok := gatewayClassNames[string(gateway.Spec.GatewayClassName)]; !ok { + continue + } + + gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), client, gateway, conf)...) } - return conf, nil + p.loadHTTPRoutes(ctx, client, gatewayListeners, conf) + + if p.ExperimentalChannel { + p.loadTCPRoutes(ctx, client, gatewayListeners, conf) + p.loadTLSRoutes(ctx, client, gatewayListeners, conf) + } + + for _, gateway := range gateways { + logger := log.Ctx(ctx).With(). + Str("gateway", gateway.Name). + Str("namespace", gateway.Namespace). + Logger() + + var listeners []gatewayListener + for _, listener := range gatewayListeners { + if listener.GWName == gateway.Name && listener.GWNamespace == gateway.Namespace { + listeners = append(listeners, listener) + } + } + + gatewayStatus, errG := p.makeGatewayStatus(gateway, listeners, addresses) + if err = client.UpdateGatewayStatus(gateway, gatewayStatus); err != nil { + logger.Error(). + Err(err). + Msg("Unable to update Gateway status") + } + if errG != nil { + logger.Error(). + Err(errG). + Msg("Unable to create Gateway status") + } + } + + return conf } -func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway *gatev1.Gateway, conf *dynamic.Configuration, tlsConfigs map[string]*tls.CertAndStores) ([]gatev1.ListenerStatus, map[ktypes.NamespacedName][]gatev1.RouteParentStatus) { - logger := log.Ctx(ctx) +func (p *Provider) loadGatewayListeners(ctx context.Context, client Client, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener { + tlsConfigs := make(map[string]*tls.CertAndStores) allocatedListeners := make(map[string]struct{}) - listenerStatuses := make([]gatev1.ListenerStatus, len(gateway.Spec.Listeners)) - httpRouteParentStatuses := make(map[ktypes.NamespacedName][]gatev1.RouteParentStatus) + gatewayListeners := make([]gatewayListener, len(gateway.Spec.Listeners)) for i, listener := range gateway.Spec.Listeners { - listenerStatuses[i] = gatev1.ListenerStatus{ - Name: listener.Name, - SupportedKinds: []gatev1.RouteGroupKind{}, - Conditions: []metav1.Condition{}, - // AttachedRoutes: 0 TODO Set to number of Routes associated with a Listener regardless of Gateway or Route status + gatewayListeners[i] = gatewayListener{ + Name: string(listener.Name), + GWName: gateway.Name, + GWNamespace: gateway.Namespace, + GWGeneration: gateway.Generation, + Protocol: listener.Protocol, + TLS: listener.TLS, + Hostname: listener.Hostname, + Status: &gatev1.ListenerStatus{ + Name: listener.Name, + SupportedKinds: []gatev1.RouteGroupKind{}, + Conditions: []metav1.Condition{}, + }, + } + + ep, err := p.entryPointName(listener.Port, listener.Protocol) + if err != nil { + // update "Detached" status with "PortUnavailable" reason + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ + Type: string(gatev1.ListenerConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.ListenerReasonPortUnavailable), + Message: fmt.Sprintf("Cannot find entryPoint for Gateway: %v", err), + }) + + continue + } + gatewayListeners[i].EPName = ep + + allowedRoutes := ptr.Deref(listener.AllowedRoutes, gatev1.AllowedRoutes{Namespaces: &gatev1.RouteNamespaces{From: ptr.To(gatev1.NamespacesFromSame)}}) + gatewayListeners[i].AllowedNamespaces, err = allowedNamespaces(client, gateway.Namespace, allowedRoutes.Namespaces) + if err != nil { + // update "ResolvedRefs" status true with "InvalidRoutesRef" reason + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ + Type: string(gatev1.ListenerConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: "InvalidRouteNamespacesSelector", // Should never happen as the selector is validated by kubernetes + Message: fmt.Sprintf("Invalid route namespaces selector: %v", err), + }) + + continue } supportedKinds, conditions := supportedRouteKinds(listener.Protocol, p.ExperimentalChannel) if len(conditions) > 0 { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, conditions...) + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, conditions...) continue } - routeKinds, conditions := getAllowedRouteKinds(gateway, listener, supportedKinds) - listenerStatuses[i].SupportedKinds = routeKinds + routeKinds, conditions := allowedRouteKinds(gateway, listener, supportedKinds) + for _, kind := range routeKinds { + gatewayListeners[i].AllowedRouteKinds = append(gatewayListeners[i].AllowedRouteKinds, string(kind.Kind)) + } + gatewayListeners[i].Status.SupportedKinds = routeKinds if len(conditions) > 0 { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, conditions...) + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, conditions...) continue } listenerKey := makeListenerKey(listener) if _, ok := allocatedListeners[listenerKey]; ok { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionConflicted), Status: metav1.ConditionTrue, ObservedGeneration: gateway.Generation, @@ -463,23 +467,8 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * allocatedListeners[listenerKey] = struct{}{} - ep, err := p.entryPointName(listener.Port, listener.Protocol) - if err != nil { - // update "Detached" status with "PortUnavailable" reason - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionAccepted), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonPortUnavailable), - Message: fmt.Sprintf("Cannot find entryPoint for Gateway: %v", err), - }) - - continue - } - if (listener.Protocol == gatev1.HTTPProtocolType || listener.Protocol == gatev1.TCPProtocolType) && listener.TLS != nil { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionAccepted), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, @@ -495,7 +484,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * if listener.Protocol == gatev1.HTTPSProtocolType || listener.Protocol == gatev1.TLSProtocolType { if listener.TLS == nil || (len(listener.TLS.CertificateRefs) == 0 && listener.TLS.Mode != nil && *listener.TLS.Mode != gatev1.TLSModePassthrough) { // update "Detached" status with "UnsupportedProtocol" reason - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionAccepted), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, @@ -517,7 +506,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * if isTLSPassthrough && len(listener.TLS.CertificateRefs) > 0 { // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayTLSConfig - logger.Warn().Msg("In case of Passthrough TLS mode, no TLS settings take effect as the TLS session from the client is NOT terminated at the Gateway") + log.Ctx(ctx).Warn().Msg("In case of Passthrough TLS mode, no TLS settings take effect as the TLS session from the client is NOT terminated at the Gateway") } // Allowed configurations: @@ -525,7 +514,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * // Protocol TLS -> Terminate -> TLSRoute/TCPRoute // Protocol HTTPS -> Terminate -> HTTPRoute if listener.Protocol == gatev1.HTTPSProtocolType && isTLSPassthrough { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionAccepted), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, @@ -540,7 +529,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * if !isTLSPassthrough { if len(listener.TLS.CertificateRefs) == 0 { // update "ResolvedRefs" status true with "InvalidCertificateRef" reason - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, @@ -558,7 +547,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) { // update "ResolvedRefs" status true with "InvalidCertificateRef" reason - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, @@ -576,9 +565,9 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * } if certificateNamespace != gateway.Namespace { - referenceGrants, err := client.GetReferenceGrants(certificateNamespace) + referenceGrants, err := client.ListReferenceGrants(certificateNamespace) if err != nil { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, @@ -586,13 +575,14 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * Reason: string(gatev1.ListenerReasonRefNotPermitted), Message: fmt.Sprintf("Cannot find any ReferenceGrant: %v", err), }) + continue } referenceGrants = filterReferenceGrantsFrom(referenceGrants, "gateway.networking.k8s.io", "Gateway", gateway.Namespace) referenceGrants = filterReferenceGrantsTo(referenceGrants, groupCore, "Secret", string(certificateRef.Name)) if len(referenceGrants) == 0 { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, @@ -611,7 +601,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * if err != nil { // update "ResolvedRefs" status false with "InvalidCertificateRef" reason // update "Programmed" status false with "Invalid" reason - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, @@ -637,33 +627,23 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * } } - for _, routeKind := range routeKinds { - switch routeKind.Kind { - case kindHTTPRoute: - listenerConditions, routeStatuses := p.gatewayHTTPRouteToHTTPConf(ctx, ep, listener, gateway, client, conf) - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, listenerConditions...) - for nsName, status := range routeStatuses { - httpRouteParentStatuses[nsName] = append(httpRouteParentStatuses[nsName], status) - } - - case kindTCPRoute: - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, gatewayTCPRouteToTCPConf(ctx, ep, listener, gateway, client, conf)...) - case kindTLSRoute: - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, gatewayTLSRouteToTCPConf(ctx, ep, listener, gateway, client, conf)...) - } - } + gatewayListeners[i].Attached = true } - return listenerStatuses, httpRouteParentStatuses + if len(tlsConfigs) > 0 { + conf.TLS.Certificates = append(conf.TLS.Certificates, getTLSConfig(tlsConfigs)...) + } + + return gatewayListeners } -func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses []gatev1.ListenerStatus, addresses []gatev1.GatewayStatusAddress) (gatev1.GatewayStatus, error) { +func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listeners []gatewayListener, addresses []gatev1.GatewayStatusAddress) (gatev1.GatewayStatus, error) { gatewayStatus := gatev1.GatewayStatus{Addresses: addresses} var result error - for i, listener := range listenerStatuses { - if len(listener.Conditions) == 0 { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, + for _, listener := range listeners { + if len(listener.Status.Conditions) == 0 { + listener.Status.Conditions = append(listener.Status.Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionAccepted), Status: metav1.ConditionTrue, @@ -690,14 +670,17 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [ }, ) + // TODO: refactor + gatewayStatus.Listeners = append(gatewayStatus.Listeners, *listener.Status) continue } - for _, condition := range listener.Conditions { + for _, condition := range listener.Status.Conditions { result = multierror.Append(result, errors.New(condition.Message)) } + + gatewayStatus.Listeners = append(gatewayStatus.Listeners, *listener.Status) } - gatewayStatus.Listeners = listenerStatuses if result != nil { // GatewayConditionReady "Ready", GatewayConditionReason "ListenersNotValid" @@ -806,524 +789,6 @@ func (p *Provider) entryPointName(port gatev1.PortNumber, protocol gatev1.Protoc return "", fmt.Errorf("no matching entryPoint for port %d and protocol %q", port, protocol) } -func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, listener gatev1.Listener, gateway *gatev1.Gateway, client Client, conf *dynamic.Configuration) ([]metav1.Condition, map[ktypes.NamespacedName]gatev1.RouteParentStatus) { - // Should not happen due to validation. - if listener.AllowedRoutes == nil { - return nil, nil - } - - namespaces, err := getRouteBindingSelectorNamespace(client, gateway.Namespace, listener.AllowedRoutes.Namespaces) - if err != nil { - // update "ResolvedRefs" status true with "InvalidRoutesRef" reason - return []metav1.Condition{{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidRouteNamespacesSelector", // Should never happen as the selector is validated by kubernetes - Message: fmt.Sprintf("Invalid route namespaces selector: %v", err), - }}, nil - } - - routes, err := client.GetHTTPRoutes(namespaces) - if err != nil { - // update "ResolvedRefs" status true with "RefNotPermitted" reason - return []metav1.Condition{{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonRefNotPermitted), - Message: fmt.Sprintf("Cannot fetch HTTPRoutes: %v", err), - }}, nil - } - - if len(routes) == 0 { - log.Ctx(ctx).Debug().Msg("No HTTPRoutes found") - return nil, nil - } - - routeStatuses := map[ktypes.NamespacedName]gatev1.RouteParentStatus{} - for _, route := range routes { - routeNsName := ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name} - - parentRef, ok := shouldAttach(gateway, listener, route.Namespace, route.Spec.CommonRouteSpec) - if !ok { - // TODO: to add an invalid HTTPRoute status when no parent is matching, - // we have to start the attachment evaluation from the route not from the listeners. - // This will fix the HTTPRouteInvalidParentRefNotMatchingSectionName test. - continue - } - - routeConditions := []metav1.Condition{ - { - Type: string(gatev1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonAccepted), - }, - { - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteConditionResolvedRefs), - }, - } - - hostnames := matchingHostnames(listener, route.Spec.Hostnames) - if len(hostnames) == 0 && listener.Hostname != nil && *listener.Hostname != "" && len(route.Spec.Hostnames) > 0 { - // TODO update the corresponding route parent status. - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.TLSRoute - continue - } - - hostRule, err := hostRule(hostnames) - if err != nil { - // TODO update the route status condition. - continue - } - - for _, routeRule := range route.Spec.Rules { - rule, err := extractRule(routeRule, hostRule) - if err != nil { - // TODO update the route status condition. - continue - } - - router := dynamic.Router{ - Rule: rule, - RuleSyntax: "v3", - EntryPoints: []string{ep}, - } - - if listener.Protocol == gatev1.HTTPSProtocolType && listener.TLS != nil { - // TODO support let's encrypt. - router.TLS = &dynamic.RouterTLSConfig{} - } - - // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. - routerName := route.Name + "-" + gateway.Name + "-" + ep - routerKey, err := makeRouterKey(router.Rule, makeID(route.Namespace, routerName)) - if err != nil { - // TODO update the route status condition. - continue - } - - middlewares, err := p.loadMiddlewares(listener, route.Namespace, routerKey, routeRule.Filters) - if err != nil { - // TODO update the route status condition. - continue - } - - for middlewareName, middleware := range middlewares { - // If the middleware is not defined in the return of the loadMiddlewares function, it means we just need a reference to that middleware. - if middleware != nil { - conf.HTTP.Middlewares[middlewareName] = middleware - } - - router.Middlewares = append(router.Middlewares, middlewareName) - } - - if len(routeRule.BackendRefs) == 0 { - continue - } - - // Traefik internal service can be used only if there is only one BackendRef service reference. - if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef) { - router.Service = string(routeRule.BackendRefs[0].Name) - } else { - var wrr dynamic.WeightedRoundRobin - for _, backendRef := range routeRule.BackendRefs { - weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1))) - - name, svc, errCondition := p.loadHTTPService(client, route, backendRef) - if errCondition != nil { - routeConditions = appendCondition(routeConditions, *errCondition) - wrr.Services = append(wrr.Services, dynamic.WRRService{ - Name: name, - Weight: weight, - Status: ptr.To(500), - }) - continue - } - - if svc != nil { - conf.HTTP.Services[name] = svc - } - - wrr.Services = append(wrr.Services, dynamic.WRRService{ - Name: name, - Weight: weight, - }) - } - - wrrName := provider.Normalize(routerKey + "-wrr") - conf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr} - - router.Service = wrrName - } - - rt := &router - p.applyRouterTransform(ctx, rt, route) - - routerKey = provider.Normalize(routerKey) - conf.HTTP.Routers[routerKey] = rt - } - - routeStatuses[routeNsName] = gatev1.RouteParentStatus{ - ParentRef: parentRef, - ControllerName: controllerName, - Conditions: routeConditions, - } - } - - return nil, routeStatuses -} - -// loadHTTPService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef. -// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc). -func (p *Provider) loadHTTPService(client Client, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, *metav1.Condition) { - group := groupCore - if backendRef.Group != nil && *backendRef.Group != "" { - group = string(*backendRef.Group) - } - - kind := ptr.Deref(backendRef.Kind, "Service") - namespace := ptr.Deref(backendRef.Namespace, gatev1.Namespace(route.Namespace)) - namespaceStr := string(namespace) - serviceName := provider.Normalize(makeID(namespaceStr, string(backendRef.Name))) - - if group != groupCore || kind != "Service" { - // TODO support cross namespace through ReferencePolicy. - if namespaceStr != route.Namespace { - return serviceName, nil, &metav1.Condition{ - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonRefNotPermitted), - Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s namespace not allowed", group, kind, namespace, backendRef.Name), - } - } - - name, service, err := p.loadHTTPBackendRef(namespaceStr, backendRef) - if err != nil { - return serviceName, nil, &metav1.Condition{ - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonInvalidKind), - Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), - } - } - - return name, service, nil - } - - port := ptr.Deref(backendRef.Port, gatev1.PortNumber(0)) - if port == 0 { - return serviceName, nil, &metav1.Condition{ - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonUnsupportedProtocol), - Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s port is required", group, kind, namespace, backendRef.Name), - } - } - - portStr := strconv.FormatInt(int64(port), 10) - serviceName = provider.Normalize(serviceName + "-" + portStr) - - lb, err := loadHTTPServers(client, namespaceStr, backendRef) - if err != nil { - return serviceName, nil, &metav1.Condition{ - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonBackendNotFound), - Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), - } - } - - return serviceName, &dynamic.Service{LoadBalancer: lb}, nil -} - -func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, error) { - // Support for cross-provider references (e.g: api@internal). - // This provides the same behavior as for IngressRoutes. - if *backendRef.Kind == "TraefikService" && strings.Contains(string(backendRef.Name), "@") { - return string(backendRef.Name), nil, nil - } - - backendFunc, ok := p.groupKindBackendFuncs[string(*backendRef.Group)][string(*backendRef.Kind)] - if !ok { - return "", nil, fmt.Errorf("unsupported HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) - } - if backendFunc == nil { - return "", nil, fmt.Errorf("undefined backendFunc for HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) - } - - return backendFunc(string(backendRef.Name), namespace) -} - -func (p *Provider) loadMiddlewares(listener gatev1.Listener, namespace string, prefix string, filters []gatev1.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) { - middlewares := make(map[string]*dynamic.Middleware) - - // The spec allows for an empty string in which case we should use the - // scheme of the request which in this case is the listener scheme. - var listenerScheme string - switch listener.Protocol { - case gatev1.HTTPProtocolType: - listenerScheme = "http" - case gatev1.HTTPSProtocolType: - listenerScheme = "https" - default: - return nil, fmt.Errorf("invalid listener protocol %s", listener.Protocol) - } - - for i, filter := range filters { - var middleware *dynamic.Middleware - switch filter.Type { - case gatev1.HTTPRouteFilterRequestRedirect: - var err error - middleware, err = createRedirectRegexMiddleware(listenerScheme, filter.RequestRedirect) - if err != nil { - return nil, fmt.Errorf("creating RedirectRegex middleware: %w", err) - } - - middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) - middlewares[middlewareName] = middleware - case gatev1.HTTPRouteFilterExtensionRef: - name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef) - if err != nil { - return nil, fmt.Errorf("unsupported filter %s: %w", filter.Type, err) - } - - middlewares[name] = middleware - - case gatev1.HTTPRouteFilterRequestHeaderModifier: - middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) - middlewares[middlewareName] = createRequestHeaderModifier(filter.RequestHeaderModifier) - - default: - // As per the spec: - // https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional - // In all cases where incompatible or unsupported filters are - // specified, implementations MUST add a warning condition to - // status. - return nil, fmt.Errorf("unsupported filter %s", filter.Type) - } - } - - return middlewares, nil -} - -func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRef *gatev1.LocalObjectReference) (string, *dynamic.Middleware, error) { - if extensionRef == nil { - return "", nil, errors.New("filter extension ref undefined") - } - - filterFunc, ok := p.groupKindFilterFuncs[string(extensionRef.Group)][string(extensionRef.Kind)] - if !ok { - return "", nil, fmt.Errorf("unsupported filter extension ref %s/%s/%s", extensionRef.Group, extensionRef.Kind, extensionRef.Name) - } - if filterFunc == nil { - return "", nil, fmt.Errorf("undefined filterFunc for filter extension ref %s/%s/%s", extensionRef.Group, extensionRef.Kind, extensionRef.Name) - } - - return filterFunc(string(extensionRef.Name), namespace) -} - -// TODO support cross namespace through ReferencePolicy. -func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) { - service, exists, err := client.GetService(namespace, string(backendRef.Name)) - if err != nil { - return nil, fmt.Errorf("getting service: %w", err) - } - if !exists { - return nil, errors.New("service not found") - } - - var portSpec corev1.ServicePort - var match bool - - for _, p := range service.Spec.Ports { - if backendRef.Port == nil || p.Port == int32(*backendRef.Port) { - portSpec = p - match = true - break - } - } - if !match { - return nil, errors.New("service port not found") - } - - endpoints, endpointsExists, err := client.GetEndpoints(namespace, string(backendRef.Name)) - if err != nil { - return nil, fmt.Errorf("getting endpoints: %w", err) - } - if !endpointsExists { - return nil, errors.New("endpoints not found") - } - - if len(endpoints.Subsets) == 0 { - return nil, errors.New("subset not found") - } - - lb := &dynamic.ServersLoadBalancer{} - lb.SetDefaults() - - var port int32 - var portStr string - for _, subset := range endpoints.Subsets { - for _, p := range subset.Ports { - if portSpec.Name == p.Name { - port = p.Port - break - } - } - - if port == 0 { - return nil, errors.New("cannot define a port") - } - - protocol := getProtocol(portSpec) - - portStr = strconv.FormatInt(int64(port), 10) - for _, addr := range subset.Addresses { - lb.Servers = append(lb.Servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(addr.IP, portStr)), - }) - } - } - - return lb, nil -} - -// loadTCPServices is generating a WRR service, even when there is only one target. -func loadTCPServices(client Client, namespace string, backendRefs []gatev1.BackendRef) (*dynamic.TCPService, map[string]*dynamic.TCPService, error) { - services := map[string]*dynamic.TCPService{} - - wrrSvc := &dynamic.TCPService{ - Weighted: &dynamic.TCPWeightedRoundRobin{ - Services: []dynamic.TCPWRRService{}, - }, - } - - for _, backendRef := range backendRefs { - if backendRef.Group == nil || backendRef.Kind == nil { - // Should not happen as this is validated by kubernetes - continue - } - - if isInternalService(backendRef) { - return nil, nil, fmt.Errorf("traefik internal service %s is not allowed in a WRR loadbalancer", backendRef.Name) - } - - weight := int(ptr.Deref(backendRef.Weight, 1)) - - if isTraefikService(backendRef) { - wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.TCPWRRService{Name: string(backendRef.Name), Weight: &weight}) - continue - } - - if *backendRef.Group != "" && *backendRef.Group != groupCore && *backendRef.Kind != "Service" { - return nil, nil, fmt.Errorf("unsupported BackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) - } - - svc := dynamic.TCPService{ - LoadBalancer: &dynamic.TCPServersLoadBalancer{}, - } - - service, exists, err := client.GetService(namespace, string(backendRef.Name)) - if err != nil { - return nil, nil, err - } - - if !exists { - return nil, nil, errors.New("service not found") - } - - if len(service.Spec.Ports) > 1 && backendRef.Port == nil { - // If the port is unspecified and the backend is a Service - // object consisting of multiple port definitions, the route - // must be dropped from the Gateway. The controller should - // raise the "ResolvedRefs" condition on the Gateway with the - // "DroppedRoutes" reason. The gateway status for this route - // should be updated with a condition that describes the error - // more specifically. - log.Error().Msg("A multiple ports Kubernetes Service cannot be used if unspecified backendRef.Port") - continue - } - - var portSpec corev1.ServicePort - var match bool - - for _, p := range service.Spec.Ports { - if backendRef.Port == nil || p.Port == int32(*backendRef.Port) { - portSpec = p - match = true - break - } - } - - if !match { - return nil, nil, errors.New("service port not found") - } - - endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, string(backendRef.Name)) - if endpointsErr != nil { - return nil, nil, endpointsErr - } - - if !endpointsExists { - return nil, nil, errors.New("endpoints not found") - } - - if len(endpoints.Subsets) == 0 { - return nil, nil, errors.New("subset not found") - } - - var port int32 - var portStr string - for _, subset := range endpoints.Subsets { - for _, p := range subset.Ports { - if portSpec.Name == p.Name { - port = p.Port - break - } - } - - if port == 0 { - return nil, nil, errors.New("cannot define a port") - } - - portStr = strconv.FormatInt(int64(port), 10) - for _, addr := range subset.Addresses { - svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.TCPServer{ - Address: net.JoinHostPort(addr.IP, portStr), - }) - } - } - - serviceName := provider.Normalize(makeID(service.Namespace, service.Name) + "-" + portStr) - services[serviceName] = &svc - - wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.TCPWRRService{Name: serviceName, Weight: &weight}) - } - - if len(wrrSvc.Weighted.Services) == 0 { - return nil, nil, errors.New("no service has been created") - } - - return wrrSvc, services, nil -} - func supportedRouteKinds(protocol gatev1.ProtocolType, experimentalChannel bool) ([]gatev1.RouteGroupKind, []metav1.Condition) { group := gatev1.Group(gatev1.GroupName) @@ -1335,9 +800,9 @@ func supportedRouteKinds(protocol gatev1.ProtocolType, experimentalChannel bool) return nil, []metav1.Condition{{ Type: string(gatev1.ListenerConditionConflicted), - Status: metav1.ConditionFalse, + Status: metav1.ConditionTrue, LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonInvalidRouteKinds), + Reason: string(gatev1.ListenerReasonProtocolConflict), Message: fmt.Sprintf("Protocol %q requires the experimental channel support to be enabled, please use the `experimentalChannel` option", protocol), }} @@ -1354,7 +819,7 @@ func supportedRouteKinds(protocol gatev1.ProtocolType, experimentalChannel bool) return nil, []metav1.Condition{{ Type: string(gatev1.ListenerConditionConflicted), - Status: metav1.ConditionFalse, + Status: metav1.ConditionTrue, LastTransitionTime: metav1.Now(), Reason: string(gatev1.ListenerReasonInvalidRouteKinds), Message: fmt.Sprintf("Protocol %q requires the experimental channel support to be enabled, please use the `experimentalChannel` option", protocol), @@ -1362,24 +827,21 @@ func supportedRouteKinds(protocol gatev1.ProtocolType, experimentalChannel bool) } return nil, []metav1.Condition{{ - Type: string(gatev1.ListenerConditionAccepted), - Status: metav1.ConditionFalse, + Type: string(gatev1.ListenerConditionConflicted), + Status: metav1.ConditionTrue, LastTransitionTime: metav1.Now(), Reason: string(gatev1.ListenerReasonUnsupportedProtocol), Message: fmt.Sprintf("Unsupported listener protocol %q", protocol), }} } -func getAllowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, supportedKinds []gatev1.RouteGroupKind) ([]gatev1.RouteGroupKind, []metav1.Condition) { +func allowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, supportedKinds []gatev1.RouteGroupKind) ([]gatev1.RouteGroupKind, []metav1.Condition) { if listener.AllowedRoutes == nil || len(listener.AllowedRoutes.Kinds) == 0 { return supportedKinds, nil } - var ( - routeKinds = []gatev1.RouteGroupKind{} - conditions []metav1.Condition - ) - + var conditions []metav1.Condition + routeKinds := []gatev1.RouteGroupKind{} uniqRouteKinds := map[gatev1.Kind]struct{}{} for _, routeKind := range listener.AllowedRoutes.Kinds { var isSupported bool @@ -1411,375 +873,7 @@ func getAllowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, sup return routeKinds, conditions } -func gatewayTCPRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Listener, gateway *gatev1.Gateway, client Client, conf *dynamic.Configuration) []metav1.Condition { - if listener.AllowedRoutes == nil { - // Should not happen due to validation. - return nil - } - - namespaces, err := getRouteBindingSelectorNamespace(client, gateway.Namespace, listener.AllowedRoutes.Namespaces) - if err != nil { - // update "ResolvedRefs" status true with "InvalidRoutesRef" reason - return []metav1.Condition{{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidRouteNamespacesSelector", // TODO should never happen as the selector is validated by Kubernetes - Message: fmt.Sprintf("Invalid route namespaces selector: %v", err), - }} - } - - routes, err := client.GetTCPRoutes(namespaces) - if err != nil { - // update "ResolvedRefs" status true with "InvalidRoutesRef" reason - return []metav1.Condition{{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonRefNotPermitted), - Message: fmt.Sprintf("Cannot fetch TCPRoutes: %v", err), - }} - } - - if len(routes) == 0 { - log.Ctx(ctx).Debug().Msg("No TCPRoutes found") - return nil - } - - var conditions []metav1.Condition - for _, route := range routes { - if _, ok := shouldAttach(gateway, listener, route.Namespace, route.Spec.CommonRouteSpec); !ok { - continue - } - - router := dynamic.TCPRouter{ - Rule: "HostSNI(`*`)", - EntryPoints: []string{ep}, - RuleSyntax: "v3", - } - - if listener.Protocol == gatev1.TLSProtocolType && listener.TLS != nil { - // TODO support let's encrypt - router.TLS = &dynamic.RouterTCPTLSConfig{ - Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough, - } - } - - // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. - routerName := route.Name + "-" + gateway.Name + "-" + ep - routerKey, err := makeRouterKey("", makeID(route.Namespace, routerName)) - if err != nil { - // update "ResolvedRefs" status true with "DroppedRoutes" reason - conditions = append(conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidRouterKey", // Should never happen - Message: fmt.Sprintf("Skipping TCPRoute %s: cannot make router's key with rule %s: %v", route.Name, router.Rule, err), - }) - - // TODO update the RouteStatus condition / deduplicate conditions on listener - continue - } - - routerKey = provider.Normalize(routerKey) - - var ruleServiceNames []string - for i, rule := range route.Spec.Rules { - if rule.BackendRefs == nil { - // Should not happen due to validation. - // https://github.com/kubernetes-sigs/gateway-api/blob/v0.4.0/apis/v1alpha2/tcproute_types.go#L76 - continue - } - - wrrService, subServices, err := loadTCPServices(client, route.Namespace, rule.BackendRefs) - if err != nil { - // update "ResolvedRefs" status true with "DroppedRoutes" reason - conditions = append(conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidBackendRefs", // TODO check the spec if a proper reason is introduced at some point - Message: fmt.Sprintf("Cannot load TCPRoute service %s/%s: %v", route.Namespace, route.Name, err), - }) - - // TODO update the RouteStatus condition / deduplicate conditions on listener - continue - } - - for svcName, svc := range subServices { - conf.TCP.Services[svcName] = svc - } - - serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i) - conf.TCP.Services[serviceName] = wrrService - - ruleServiceNames = append(ruleServiceNames, serviceName) - } - - if len(ruleServiceNames) == 1 { - router.Service = ruleServiceNames[0] - conf.TCP.Routers[routerKey] = &router - continue - } - - routeServiceKey := routerKey + "-wrr" - routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}} - - for _, name := range ruleServiceNames { - service := dynamic.TCPWRRService{Name: name} - service.SetDefaults() - - routeService.Weighted.Services = append(routeService.Weighted.Services, service) - } - - conf.TCP.Services[routeServiceKey] = routeService - - router.Service = routeServiceKey - conf.TCP.Routers[routerKey] = &router - } - - return conditions -} - -func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Listener, gateway *gatev1.Gateway, client Client, conf *dynamic.Configuration) []metav1.Condition { - if listener.AllowedRoutes == nil { - // Should not happen due to validation. - return nil - } - - namespaces, err := getRouteBindingSelectorNamespace(client, gateway.Namespace, listener.AllowedRoutes.Namespaces) - if err != nil { - // update "ResolvedRefs" status true with "InvalidRoutesRef" reason - return []metav1.Condition{{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidRouteNamespacesSelector", // TODO should never happen as the selector is validated by Kubernetes - Message: fmt.Sprintf("Invalid route namespaces selector: %v", err), - }} - } - - routes, err := client.GetTLSRoutes(namespaces) - if err != nil { - // update "ResolvedRefs" status true with "InvalidRoutesRef" reason - return []metav1.Condition{{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonRefNotPermitted), - Message: fmt.Sprintf("Cannot fetch TLSRoutes: %v", err), - }} - } - - if len(routes) == 0 { - log.Ctx(ctx).Debug().Msg("No TLSRoutes found") - return nil - } - - var conditions []metav1.Condition - for _, route := range routes { - if _, ok := shouldAttach(gateway, listener, route.Namespace, route.Spec.CommonRouteSpec); !ok { - continue - } - - hostnames := matchingHostnames(listener, route.Spec.Hostnames) - if len(hostnames) == 0 && listener.Hostname != nil && *listener.Hostname != "" && len(route.Spec.Hostnames) > 0 { - for _, parent := range route.Status.Parents { - parent.Conditions = append(parent.Conditions, metav1.Condition{ - Type: string(gatev1.GatewayClassConditionStatusAccepted), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - Reason: string(gatev1.ListenerReasonHostnameConflict), - Message: fmt.Sprintf("No hostname match between listener: %v and route: %v", listener.Hostname, route.Spec.Hostnames), - LastTransitionTime: metav1.Now(), - }) - } - - continue - } - - rule, err := hostSNIRule(hostnames) - if err != nil { - // update "ResolvedRefs" status true with "InvalidHostnames" reason - conditions = append(conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidHostnames", // TODO check the spec if a proper reason is introduced at some point - Message: fmt.Sprintf("Skipping TLSRoute %s: cannot make route's SNI match: %v", route.Name, err), - }) - // TODO update the RouteStatus condition / deduplicate conditions on listener - continue - } - - router := dynamic.TCPRouter{ - Rule: rule, - RuleSyntax: "v3", - EntryPoints: []string{ep}, - TLS: &dynamic.RouterTCPTLSConfig{ - Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough, - }, - } - - // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. - routerName := route.Name + "-" + gateway.Name + "-" + ep - routerKey, err := makeRouterKey(rule, makeID(route.Namespace, routerName)) - if err != nil { - // update "ResolvedRefs" status true with "DroppedRoutes" reason - conditions = append(conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidRouterKey", // Should never happen - Message: fmt.Sprintf("Skipping TLSRoute %s: cannot make router's key with rule %s: %v", route.Name, router.Rule, err), - }) - - // TODO update the RouteStatus condition / deduplicate conditions on listener - continue - } - - routerKey = provider.Normalize(routerKey) - - var ruleServiceNames []string - for i, routeRule := range route.Spec.Rules { - if len(routeRule.BackendRefs) == 0 { - // Should not happen due to validation. - // https://github.com/kubernetes-sigs/gateway-api/blob/v0.4.0/apis/v1alpha2/tlsroute_types.go#L120 - continue - } - - wrrService, subServices, err := loadTCPServices(client, route.Namespace, routeRule.BackendRefs) - if err != nil { - // update "ResolvedRefs" status true with "InvalidBackendRefs" reason - conditions = append(conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "InvalidBackendRefs", // TODO check the spec if a proper reason is introduced at some point - Message: fmt.Sprintf("Cannot load TLSRoute service %s/%s: %v", route.Namespace, route.Name, err), - }) - - // TODO update the RouteStatus condition / deduplicate conditions on listener - continue - } - - for svcName, svc := range subServices { - conf.TCP.Services[svcName] = svc - } - - serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i) - conf.TCP.Services[serviceName] = wrrService - - ruleServiceNames = append(ruleServiceNames, serviceName) - } - - if len(ruleServiceNames) == 1 { - router.Service = ruleServiceNames[0] - conf.TCP.Routers[routerKey] = &router - continue - } - - routeServiceKey := routerKey + "-wrr" - routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}} - - for _, name := range ruleServiceNames { - service := dynamic.TCPWRRService{Name: name} - service.SetDefaults() - - routeService.Weighted.Services = append(routeService.Weighted.Services, service) - } - - conf.TCP.Services[routeServiceKey] = routeService - - router.Service = routeServiceKey - conf.TCP.Routers[routerKey] = &router - } - - return conditions -} - -// Because of Kubernetes validation we admit that the given Hostnames are valid. -// https://github.com/kubernetes-sigs/gateway-api/blob/ff9883da4cad8554cd300394f725ab3a27502785/apis/v1alpha2/shared_types.go#L252 -func matchingHostnames(listener gatev1.Listener, hostnames []gatev1.Hostname) []gatev1.Hostname { - if listener.Hostname == nil || *listener.Hostname == "" { - return hostnames - } - - if len(hostnames) == 0 { - return []gatev1.Hostname{*listener.Hostname} - } - - listenerLabels := strings.Split(string(*listener.Hostname), ".") - - var matches []gatev1.Hostname - - for _, hostname := range hostnames { - if hostname == *listener.Hostname { - matches = append(matches, hostname) - continue - } - - hostnameLabels := strings.Split(string(hostname), ".") - if len(listenerLabels) != len(hostnameLabels) { - continue - } - - if !slices.Equal(listenerLabels[1:], hostnameLabels[1:]) { - continue - } - - if listenerLabels[0] == "*" { - matches = append(matches, hostname) - continue - } - - if hostnameLabels[0] == "*" { - matches = append(matches, *listener.Hostname) - continue - } - } - - return matches -} - -func shouldAttach(gateway *gatev1.Gateway, listener gatev1.Listener, routeNamespace string, routeSpec gatev1.CommonRouteSpec) (gatev1.ParentReference, bool) { - for _, parentRef := range routeSpec.ParentRefs { - if parentRef.Group == nil || *parentRef.Group != gatev1.GroupName { - continue - } - - if parentRef.Kind == nil || *parentRef.Kind != kindGateway { - continue - } - - if parentRef.SectionName != nil && *parentRef.SectionName != listener.Name { - continue - } - - namespace := routeNamespace - if parentRef.Namespace != nil { - namespace = string(*parentRef.Namespace) - } - - if namespace == gateway.Namespace && string(parentRef.Name) == gateway.Name { - return parentRef, true - } - } - - return gatev1.ParentReference{}, false -} - -func getRouteBindingSelectorNamespace(client Client, gatewayNamespace string, routeNamespaces *gatev1.RouteNamespaces) ([]string, error) { +func allowedNamespaces(client Client, gatewayNamespace string, routeNamespaces *gatev1.RouteNamespaces) ([]string, error) { if routeNamespaces == nil || routeNamespaces.From == nil { return []string{gatewayNamespace}, nil } @@ -1797,183 +891,111 @@ func getRouteBindingSelectorNamespace(client Client, gatewayNamespace string, ro return nil, fmt.Errorf("malformed selector: %w", err) } - return client.GetNamespaces(selector) + return client.ListNamespaces(selector) } return nil, fmt.Errorf("unsupported RouteSelectType: %q", *routeNamespaces.From) } -func hostRule(hostnames []gatev1.Hostname) (string, error) { - var rules []string +func findMatchingHostnames(listenerHostname *gatev1.Hostname, routeHostnames []gatev1.Hostname) ([]gatev1.Hostname, bool) { + if listenerHostname == nil { + return routeHostnames, true + } - for _, hostname := range hostnames { - host := string(hostname) - // When unspecified, "", or *, all hostnames are matched. - // This field can be omitted for protocols that don't require hostname based matching. - // TODO Refactor this when building support for TLS options. - if host == "*" || host == "" { - return "", nil - } + if len(routeHostnames) == 0 { + return []gatev1.Hostname{*listenerHostname}, true + } - wildcard := strings.Count(host, "*") - if wildcard == 0 { - rules = append(rules, fmt.Sprintf("Host(`%s`)", host)) + var matches []gatev1.Hostname + for _, routeHostname := range routeHostnames { + if match := findMatchingHostname(*listenerHostname, routeHostname); match != "" { + matches = append(matches, match) continue } - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Hostname - if !strings.HasPrefix(host, "*.") || wildcard > 1 { - return "", fmt.Errorf("invalid rule: %q", host) + if match := findMatchingHostname(routeHostname, *listenerHostname); match != "" { + matches = append(matches, match) + continue } - - host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1) - rules = append(rules, fmt.Sprintf("HostRegexp(`^%s$`)", host)) } - switch len(rules) { - case 0: - return "", nil - case 1: - return rules[0], nil - default: - return fmt.Sprintf("(%s)", strings.Join(rules, " || ")), nil - } + return matches, len(matches) > 0 } -func hostSNIRule(hostnames []gatev1.Hostname) (string, error) { - rules := make([]string, 0, len(hostnames)) - uniqHostnames := map[gatev1.Hostname]struct{}{} - - for _, hostname := range hostnames { - if len(hostname) == 0 { - continue - } - - if _, exists := uniqHostnames[hostname]; exists { - continue - } - - host := string(hostname) - uniqHostnames[hostname] = struct{}{} - - wildcard := strings.Count(host, "*") - if wildcard == 0 { - rules = append(rules, fmt.Sprintf("HostSNI(`%s`)", host)) - continue - } - - if !strings.HasPrefix(host, "*.") || wildcard > 1 { - return "", fmt.Errorf("invalid rule: %q", host) - } - - host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1) - rules = append(rules, fmt.Sprintf("HostSNIRegexp(`^%s$`)", host)) +func findMatchingHostname(h1, h2 gatev1.Hostname) gatev1.Hostname { + if h1 == h2 { + return h1 } - if len(hostnames) == 0 || len(rules) == 0 { - return "HostSNI(`*`)", nil + if !strings.HasPrefix(string(h1), "*.") { + return "" } - return strings.Join(rules, " || "), nil + trimmedH1 := strings.TrimPrefix(string(h1), "*") + // root domain doesn't match subdomain wildcard. + if trimmedH1 == string(h2) { + return "" + } + + if !strings.HasSuffix(string(h2), trimmedH1) { + return "" + } + + return lessWildcards(h1, h2) } -func extractRule(routeRule gatev1.HTTPRouteRule, hostRule string) (string, error) { - var rule string - var matchesRules []string - - for _, match := range routeRule.Matches { - if (match.Path == nil || match.Path.Type == nil) && match.Headers == nil { - continue - } - - var matchRules []string - - if match.Path != nil && match.Path.Type != nil && match.Path.Value != nil { - switch *match.Path.Type { - case gatev1.PathMatchExact: - matchRules = append(matchRules, fmt.Sprintf("Path(`%s`)", *match.Path.Value)) - case gatev1.PathMatchPathPrefix: - matchRules = append(matchRules, buildPathMatchPathPrefixRule(*match.Path.Value)) - case gatev1.PathMatchRegularExpression: - matchRules = append(matchRules, fmt.Sprintf("PathRegexp(`%s`)", *match.Path.Value)) - default: - return "", fmt.Errorf("unsupported path match type %s", *match.Path.Type) - } - } - - headerRules, err := extractHeaderRules(match.Headers) - if err != nil { - return "", err - } - - matchRules = append(matchRules, headerRules...) - matchesRules = append(matchesRules, strings.Join(matchRules, " && ")) +func lessWildcards(h1, h2 gatev1.Hostname) gatev1.Hostname { + if strings.Count(string(h1), "*") > strings.Count(string(h2), "*") { + return h2 } - // If no matches are specified, the default is a prefix - // path match on "/", which has the effect of matching every - // HTTP request. - if len(routeRule.Matches) == 0 { - matchesRules = append(matchesRules, "PathPrefix(`/`)") - } - - if hostRule != "" { - if len(matchesRules) == 0 { - return hostRule, nil - } - rule += hostRule + " && " - } - - if len(matchesRules) == 1 { - return rule + matchesRules[0], nil - } - - if len(rule) == 0 { - return strings.Join(matchesRules, " || "), nil - } - - return rule + "(" + strings.Join(matchesRules, " || ") + ")", nil + return h1 } -func extractHeaderRules(headers []gatev1.HTTPHeaderMatch) ([]string, error) { - var headerRules []string - - // TODO handle other headers types - for _, header := range headers { - if header.Type == nil { - // Should never happen due to kubernetes validation. - continue - } - - switch *header.Type { - case gatev1.HeaderMatchExact: - headerRules = append(headerRules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value)) - default: - return nil, fmt.Errorf("unsupported header match type %s", *header.Type) - } +func allowRoute(listener gatewayListener, routeNamespace, routeKind string) bool { + if !slices.Contains(listener.AllowedRouteKinds, routeKind) { + return false } - return headerRules, nil + return slices.ContainsFunc(listener.AllowedNamespaces, func(allowedNamespace string) bool { + return allowedNamespace == corev1.NamespaceAll || allowedNamespace == routeNamespace + }) } -func buildPathMatchPathPrefixRule(path string) string { - if path == "/" { - return "PathPrefix(`/`)" +func matchListener(listener gatewayListener, routeNamespace string, parentRef gatev1.ParentReference) bool { + if ptr.Deref(parentRef.Group, gatev1.GroupName) != gatev1.GroupName { + return false } - path = strings.TrimSuffix(path, "/") - return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", path) + if ptr.Deref(parentRef.Kind, kindGateway) != kindGateway { + return false + } + + parentRefNamespace := string(ptr.Deref(parentRef.Namespace, gatev1.Namespace(routeNamespace))) + if listener.GWNamespace != parentRefNamespace { + return false + } + + if string(parentRef.Name) != listener.GWName { + return false + } + + sectionName := string(ptr.Deref(parentRef.SectionName, "")) + if sectionName != "" && sectionName != listener.Name { + return false + } + + return true } -func makeRouterKey(rule, name string) (string, error) { +func makeRouterKey(rule, name string) string { h := sha256.New() - if _, err := h.Write([]byte(rule)); err != nil { - return "", err - } - key := fmt.Sprintf("%s-%.10x", name, h.Sum(nil)) + // As explained in https://pkg.go.dev/hash#Hash, + // Write never returns an error. + h.Write([]byte(rule)) - return key, nil + return fmt.Sprintf("%s-%.10x", name, h.Sum(nil)) } func makeID(namespace, name string) string { @@ -2057,76 +1079,6 @@ func getCertificateBlocks(secret *corev1.Secret, namespace, secretName string) ( return cert, key, nil } -// createRequestHeaderModifier does not enforce/check the configuration, -// as the spec indicates that either the webhook or CEL (since v1.0 GA Release) should enforce that. -func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middleware { - sets := map[string]string{} - for _, header := range filter.Set { - sets[string(header.Name)] = header.Value - } - - adds := map[string]string{} - for _, header := range filter.Add { - adds[string(header.Name)] = header.Value - } - - return &dynamic.Middleware{ - RequestHeaderModifier: &dynamic.RequestHeaderModifier{ - Set: sets, - Add: adds, - Remove: filter.Remove, - }, - } -} - -func createRedirectRegexMiddleware(scheme string, filter *gatev1.HTTPRequestRedirectFilter) (*dynamic.Middleware, error) { - // Use the HTTPRequestRedirectFilter scheme if defined. - filterScheme := scheme - if filter.Scheme != nil { - filterScheme = *filter.Scheme - } - - if filterScheme != "http" && filterScheme != "https" { - return nil, fmt.Errorf("invalid scheme %s", filterScheme) - } - - statusCode := http.StatusFound - if filter.StatusCode != nil { - statusCode = *filter.StatusCode - } - - if statusCode != http.StatusMovedPermanently && statusCode != http.StatusFound { - return nil, fmt.Errorf("invalid status code %d", statusCode) - } - - port := "${port}" - if filter.Port != nil { - port = fmt.Sprintf(":%d", *filter.Port) - } - - hostname := "${hostname}" - if filter.Hostname != nil && *filter.Hostname != "" { - hostname = string(*filter.Hostname) - } - - return &dynamic.Middleware{ - RedirectRegex: &dynamic.RedirectRegex{ - Regex: `^[a-z]+:\/\/(?P.+@)?(?P\[[\w:\.]+\]|[\w\._-]+)(?P:\d+)?\/(?P.*)`, - Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port), - Permanent: statusCode == http.StatusMovedPermanently, - }, - }, nil -} - -func getProtocol(portSpec corev1.ServicePort) string { - protocol := "http" - if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { - protocol = "https" - } - - return protocol -} - func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} { if throttleDuration == 0 { return nil @@ -2241,74 +1193,6 @@ func kindToString(p *gatev1.Kind) string { return string(*p) } -func makeHTTPRouteStatuses(gwNs string, routeParentStatuses map[ktypes.NamespacedName][]gatev1.RouteParentStatus) map[ktypes.NamespacedName]gatev1.HTTPRouteStatus { - res := map[ktypes.NamespacedName]gatev1.HTTPRouteStatus{} - - for nsName, parentStatuses := range routeParentStatuses { - var httpRouteStatus gatev1.HTTPRouteStatus - for _, parentStatus := range parentStatuses { - exists := slices.ContainsFunc(httpRouteStatus.Parents, func(status gatev1.RouteParentStatus) bool { - return parentRefEquals(gwNs, parentStatus.ParentRef, status.ParentRef) - }) - if !exists { - httpRouteStatus.Parents = append(httpRouteStatus.Parents, parentStatus) - } - } - - res[nsName] = httpRouteStatus - } - - return res -} - -func parentRefEquals(gwNs string, p1, p2 gatev1.ParentReference) bool { - if !pointerEquals(p1.Group, p2.Group) { - return false - } - - if !pointerEquals(p1.Kind, p2.Kind) { - return false - } - - if !pointerEquals(p1.SectionName, p2.SectionName) { - return false - } - - if p1.Name != p2.Name { - return false - } - - p1Ns := gwNs - if p1.Namespace != nil { - p1Ns = string(*p1.Namespace) - } - - p2Ns := gwNs - if p2.Namespace != nil { - p2Ns = string(*p2.Namespace) - } - - return p1Ns == p2Ns -} - -func pointerEquals[T comparable](p1, p2 *T) bool { - if p1 == nil && p2 == nil { - return true - } - - var val1 T - if p1 != nil { - val1 = *p1 - } - - var val2 T - if p2 != nil { - val2 = *p2 - } - - return val1 == val2 -} - func appendCondition(conditions []metav1.Condition, condition metav1.Condition) []metav1.Condition { res := []metav1.Condition{condition} for _, c := range conditions { diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index d83ea3e38..033f19420 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -17,6 +17,7 @@ import ( "github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s" "github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/types" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" kubefake "k8s.io/client-go/kubernetes/fake" @@ -484,32 +485,6 @@ func TestLoadHTTPRoutes(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, - { - desc: "Empty caused unsupported HTTPRoute rule", - entryPoints: map[string]Entrypoint{"web": { - Address: ":80", - }}, - paths: []string{"services.yml", "httproute/simple_with_bad_rule.yml"}, - expected: &dynamic.Configuration{ - UDP: &dynamic.UDPConfiguration{ - Routers: map[string]*dynamic.UDPRouter{}, - Services: map[string]*dynamic.UDPService{}, - }, - TCP: &dynamic.TCPConfiguration{ - Routers: map[string]*dynamic.TCPRouter{}, - Middlewares: map[string]*dynamic.TCPMiddleware{}, - Services: map[string]*dynamic.TCPService{}, - ServersTransports: map[string]*dynamic.TCPServersTransport{}, - }, - HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, - ServersTransports: map[string]*dynamic.ServersTransport{}, - }, - TLS: &dynamic.TLSConfiguration{}, - }, - }, { desc: "Empty because no tcp route defined tls protocol", paths: []string{"services.yml", "tcproute/without_tcproute_tls_protocol.yml"}, @@ -814,16 +789,16 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0": { + "default-http-app-1-my-gateway-web-baa117c0219e3878749f": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0-wrr", - Rule: "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.com$`)) && PathPrefix(`/`)", + Service: "default-http-app-1-my-gateway-web-baa117c0219e3878749f-wrr", + Rule: "(Host(`foo.com`) || HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.com$`)) && PathPrefix(`/`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0-wrr": { + "default-http-app-1-my-gateway-web-baa117c0219e3878749f-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -874,16 +849,16 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-b0521a61fb43068694b4": { + "default-http-app-1-my-gateway-web-45eba2eaf40ac792e036": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-b0521a61fb43068694b4-wrr", - Rule: "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.foo\\.com$`)) && PathPrefix(`/`)", + Service: "default-http-app-1-my-gateway-web-45eba2eaf40ac792e036-wrr", + Rule: "(Host(`foo.com`) || HostRegexp(`^[a-z0-9-\\.]+\\.foo\\.com$`)) && PathPrefix(`/`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-b0521a61fb43068694b4-wrr": { + "default-http-app-1-my-gateway-web-45eba2eaf40ac792e036-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1809,7 +1784,7 @@ func TestLoadHTTPRoutes(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateway(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -2125,7 +2100,7 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { p.RegisterBackendFuncs(group, kind, backendFunc) } } - conf := p.loadConfigurationFromGateway(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -2287,9 +2262,28 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, + Routers: map[string]*dynamic.Router{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "invalid-httproute-filter", + Weight: ptr.To(1), + Status: ptr.To(500), + }, + }, + }, + }, + }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, TLS: &dynamic.TLSConfiguration{}, @@ -2317,9 +2311,28 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, + Routers: map[string]*dynamic.Router{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "invalid-httproute-filter", + Weight: ptr.To(1), + Status: ptr.To(500), + }, + }, + }, + }, + }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, TLS: &dynamic.TLSConfiguration{}, @@ -2357,7 +2370,7 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { p.RegisterFilterFuncs(group, kind, filterFunc) } } - conf := p.loadConfigurationFromGateway(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -3136,7 +3149,7 @@ func TestLoadTCPRoutes(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateway(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -3299,7 +3312,16 @@ func TestLoadTLSRoutes(t *testing.T) { Services: map[string]*dynamic.Service{}, ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), + KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"), + }, + }, + }, + }, }, }, { @@ -3336,32 +3358,6 @@ func TestLoadTLSRoutes(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, - { - desc: "Empty caused by simple TLSRoute with invalid SNI matching", - paths: []string{"services.yml", "tlsroute/with_invalid_SNI_matching.yml"}, - entryPoints: map[string]Entrypoint{ - "tls": {Address: ":9001"}, - }, - expected: &dynamic.Configuration{ - UDP: &dynamic.UDPConfiguration{ - Routers: map[string]*dynamic.UDPRouter{}, - Services: map[string]*dynamic.UDPService{}, - }, - TCP: &dynamic.TCPConfiguration{ - Routers: map[string]*dynamic.TCPRouter{}, - Middlewares: map[string]*dynamic.TCPMiddleware{}, - Services: map[string]*dynamic.TCPService{}, - ServersTransports: map[string]*dynamic.TCPServersTransport{}, - }, - HTTP: &dynamic.HTTPConfiguration{ - Routers: map[string]*dynamic.Router{}, - Middlewares: map[string]*dynamic.Middleware{}, - Services: map[string]*dynamic.Service{}, - ServersTransports: map[string]*dynamic.ServersTransport{}, - }, - TLS: &dynamic.TLSConfiguration{}, - }, - }, { desc: "Simple TLS listener to TCPRoute in Terminate mode", paths: []string{"services.yml", "tlsroute/simple_TLS_to_TCPRoute.yml"}, @@ -4284,7 +4280,7 @@ func TestLoadTLSRoutes(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateway(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -5317,7 +5313,7 @@ func TestLoadMixedRoutes(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateway(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -5528,747 +5524,135 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateway(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background(), client) assert.Equal(t, test.expected, conf) }) } } -func Test_hostRule(t *testing.T) { - testCases := []struct { - desc string - hostnames []gatev1.Hostname - expectedRule string - expectErr bool - }{ - { - desc: "Empty rule and matches", - expectedRule: "", - }, - { - desc: "One Host", - hostnames: []gatev1.Hostname{ - "Foo", - }, - expectedRule: "Host(`Foo`)", - }, - { - desc: "Multiple Hosts", - hostnames: []gatev1.Hostname{ - "Foo", - "Bar", - "Bir", - }, - expectedRule: "(Host(`Foo`) || Host(`Bar`) || Host(`Bir`))", - }, - { - desc: "Multiple Hosts with empty one", - hostnames: []gatev1.Hostname{ - "Foo", - "", - "Bir", - }, - expectedRule: "", - }, - { - desc: "Multiple empty hosts", - hostnames: []gatev1.Hostname{ - "", - "", - "", - }, - expectedRule: "", - }, - { - desc: "Several Host and wildcard", - hostnames: []gatev1.Hostname{ - "*.bar.foo", - "bar.foo", - "foo.foo", - }, - expectedRule: "(HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.foo$`) || Host(`bar.foo`) || Host(`foo.foo`))", - }, - { - desc: "Host with wildcard", - hostnames: []gatev1.Hostname{ - "*.bar.foo", - }, - expectedRule: "HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.foo$`)", - }, - { - desc: "Alone wildcard", - hostnames: []gatev1.Hostname{ - "*", - "*.foo.foo", - }, - }, - { - desc: "Multiple alone Wildcard", - hostnames: []gatev1.Hostname{ - "foo.foo", - "*.*", - }, - expectErr: true, - }, - { - desc: "Multiple Wildcard", - hostnames: []gatev1.Hostname{ - "foo.foo", - "*.toto.*.bar.foo", - }, - expectErr: true, - }, - { - desc: "Multiple subdomain with misplaced wildcard", - hostnames: []gatev1.Hostname{ - "foo.foo", - "toto.*.bar.foo", - }, - expectErr: true, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - rule, err := hostRule(test.hostnames) - - assert.Equal(t, test.expectedRule, rule) - if test.expectErr { - assert.Error(t, err) - } - }) - } -} - -func Test_extractRule(t *testing.T) { - testCases := []struct { - desc string - routeRule gatev1.HTTPRouteRule - hostRule string - expectedRule string - expectedError bool - }{ - { - desc: "Empty rule and matches", - expectedRule: "PathPrefix(`/`)", - }, - { - desc: "One Host rule without matches", - hostRule: "Host(`foo.com`)", - expectedRule: "Host(`foo.com`) && PathPrefix(`/`)", - }, - { - desc: "One HTTPRouteMatch with nil HTTPHeaderMatch", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - {Headers: nil}, - }, - }, - expectedRule: "", - }, - { - desc: "One HTTPRouteMatch with nil HTTPHeaderMatch Type", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Headers: []gatev1.HTTPHeaderMatch{ - {Type: nil, Name: "foo", Value: "bar"}, - }, - }, - }, - }, - expectedRule: "", - }, - { - desc: "One HTTPRouteMatch with nil HTTPPathMatch", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - {Path: nil}, - }, - }, - expectedRule: "", - }, - { - desc: "One HTTPRouteMatch with nil HTTPPathMatch Type", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: nil, - Value: ptr.To("/foo/"), - }, - }, - }, - }, - expectedRule: "", - }, - { - desc: "One HTTPRouteMatch with nil HTTPPathMatch Values", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr(gatev1.PathMatchExact), - Value: nil, - }, - }, - }, - }, - expectedRule: "", - }, - { - desc: "One Path in matches", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - }, - }, - }, - expectedRule: "Path(`/foo/`)", - }, - { - desc: "One Path in matches and another unknown", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - }, - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr("unknown"), - Value: ptr.To("/foo/"), - }, - }, - }, - }, - expectedError: true, - }, - { - desc: "One Path in matches and another empty", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - }, - {}, - }, - }, - expectedRule: "Path(`/foo/`)", - }, - { - desc: "Path OR Header rules", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - }, - { - Headers: []gatev1.HTTPHeaderMatch{ - { - Type: headerMatchTypePtr(gatev1.HeaderMatchExact), - Name: "my-header", - Value: "foo", - }, - }, - }, - }, - }, - expectedRule: "Path(`/foo/`) || Header(`my-header`,`foo`)", - }, - { - desc: "Path && Header rules", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - Headers: []gatev1.HTTPHeaderMatch{ - { - Type: headerMatchTypePtr(gatev1.HeaderMatchExact), - Name: "my-header", - Value: "foo", - }, - }, - }, - }, - }, - expectedRule: "Path(`/foo/`) && Header(`my-header`,`foo`)", - }, - { - desc: "Host && Path && Header rules", - hostRule: "Host(`foo.com`)", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - Headers: []gatev1.HTTPHeaderMatch{ - { - Type: headerMatchTypePtr(gatev1.HeaderMatchExact), - Name: "my-header", - Value: "foo", - }, - }, - }, - }, - }, - expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Header(`my-header`,`foo`)", - }, - { - desc: "Host && (Path || Header) rules", - hostRule: "Host(`foo.com`)", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: pathMatchTypePtr(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - }, - { - Headers: []gatev1.HTTPHeaderMatch{ - { - Type: headerMatchTypePtr(gatev1.HeaderMatchExact), - Name: "my-header", - Value: "foo", - }, - }, - }, - }, - }, - expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || Header(`my-header`,`foo`))", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - rule, err := extractRule(test.routeRule, test.hostRule) - if test.expectedError { - assert.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, test.expectedRule, rule) - }) - } -} - -func Test_hostSNIRule(t *testing.T) { - testCases := []struct { - desc string - hostnames []gatev1.Hostname - expectedRule string - expectError bool - }{ - { - desc: "Empty", - expectedRule: "HostSNI(`*`)", - }, - { - desc: "Empty hostname", - hostnames: []gatev1.Hostname{""}, - expectedRule: "HostSNI(`*`)", - }, - { - desc: "Unsupported wildcard", - hostnames: []gatev1.Hostname{"*"}, - expectError: true, - }, - { - desc: "Supported wildcard", - hostnames: []gatev1.Hostname{"*.foo"}, - expectedRule: "HostSNIRegexp(`^[a-zA-Z0-9-]+\\.foo$`)", - }, - { - desc: "Multiple malformed wildcard", - hostnames: []gatev1.Hostname{"*.foo.*"}, - expectError: true, - }, - { - desc: "Some empty hostnames", - hostnames: []gatev1.Hostname{"foo", "", "bar"}, - expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)", - }, - { - desc: "Valid hostname", - hostnames: []gatev1.Hostname{"foo"}, - expectedRule: "HostSNI(`foo`)", - }, - { - desc: "Multiple valid hostnames", - hostnames: []gatev1.Hostname{"foo", "bar"}, - expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)", - }, - { - desc: "Multiple valid hostnames with wildcard", - hostnames: []gatev1.Hostname{"bar.foo", "foo.foo", "*.foo"}, - expectedRule: "HostSNI(`bar.foo`) || HostSNI(`foo.foo`) || HostSNIRegexp(`^[a-zA-Z0-9-]+\\.foo$`)", - }, - { - desc: "Multiple overlapping hostnames", - hostnames: []gatev1.Hostname{"foo", "bar", "foo", "baz"}, - expectedRule: "HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`)", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - rule, err := hostSNIRule(test.hostnames) - if test.expectError { - assert.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, test.expectedRule, rule) - }) - } -} - -func Test_shouldAttach(t *testing.T) { +func Test_matchListener(t *testing.T) { testCases := []struct { desc string - gateway *gatev1.Gateway - listener gatev1.Listener + gwListener gatewayListener + parentRef gatev1.ParentReference routeNamespace string - routeSpec gatev1.CommonRouteSpec - wantAttach bool - wantParentRef gatev1.ParentReference + wantMatch bool }{ { - desc: "No ParentRefs", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, + desc: "Unsupported group", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", }, - listener: gatev1.Listener{ - Name: "foo", + parentRef: gatev1.ParentReference{ + Group: ptr.To(gatev1.Group("foo")), }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: nil, - }, - wantAttach: false, + wantMatch: false, }, { - desc: "Unsupported Kind", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, + desc: "Unsupported kind", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", }, - listener: gatev1.Listener{ - Name: "foo", + parentRef: gatev1.ParentReference{ + Group: ptr.To(gatev1.Group(gatev1.GroupName)), + Kind: ptr.To(gatev1.Kind("foo")), }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("bar"), - Name: "gateway", - Namespace: namespacePtr("default"), - Kind: kindPtr("Foo"), - Group: groupPtr(gatev1.GroupName), - }, - }, - }, - wantAttach: false, + wantMatch: false, }, { - desc: "Unsupported Group", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, + desc: "Namespace does not match the listener", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", }, - listener: gatev1.Listener{ - Name: "foo", + parentRef: gatev1.ParentReference{ + Namespace: ptr.To(gatev1.Namespace("foo")), + Group: ptr.To(gatev1.Group(gatev1.GroupName)), + Kind: ptr.To(gatev1.Kind("Gateway")), }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("bar"), - Name: "gateway", - Namespace: namespacePtr("default"), - Kind: kindPtr("Gateway"), - Group: groupPtr("foo.com"), - }, - }, - }, - wantAttach: false, + wantMatch: false, }, { - desc: "Kind is nil", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, + desc: "Route namespace defaulting does not match the listener", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", }, - listener: gatev1.Listener{ - Name: "foo", + routeNamespace: "foo", + parentRef: gatev1.ParentReference{ + Group: ptr.To(gatev1.Group(gatev1.GroupName)), + Kind: ptr.To(gatev1.Kind("Gateway")), }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("bar"), - Name: "gateway", - Namespace: namespacePtr("default"), - Group: groupPtr(gatev1.GroupName), - }, - }, - }, - wantAttach: false, + wantMatch: false, }, { - desc: "Group is nil", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, + desc: "Name does not match the listener", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", }, - listener: gatev1.Listener{ - Name: "foo", + parentRef: gatev1.ParentReference{ + Namespace: ptr.To(gatev1.Namespace("default")), + Name: "foo", + Group: ptr.To(gatev1.Group(gatev1.GroupName)), + Kind: ptr.To(gatev1.Kind("Gateway")), }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("bar"), - Name: "gateway", - Namespace: namespacePtr("default"), - Kind: kindPtr("Gateway"), - }, - }, - }, - wantAttach: false, + wantMatch: false, }, { - desc: "SectionName does not match a listener desc", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, + desc: "SectionName does not match a listener", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", }, - listener: gatev1.Listener{ - Name: "foo", - }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("bar"), - Name: "gateway", - Namespace: namespacePtr("default"), - Group: groupPtr(gatev1.GroupName), - Kind: kindPtr("Gateway"), - }, - }, - }, - wantAttach: false, - }, - { - desc: "Namespace does not match the Gateway namespace", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, - }, - listener: gatev1.Listener{ - Name: "foo", - }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("bar"), - Name: "gateway", - Namespace: namespacePtr("bar"), - Group: groupPtr(gatev1.GroupName), - Kind: kindPtr("Gateway"), - }, - }, - }, - wantAttach: false, - }, - { - desc: "Route namespace does not match the Gateway namespace", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, - }, - listener: gatev1.Listener{ - Name: "foo", - }, - routeNamespace: "bar", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("bar"), - Name: "gateway", - Group: groupPtr(gatev1.GroupName), - Kind: kindPtr("Gateway"), - }, - }, - }, - wantAttach: false, - }, - { - desc: "Unsupported Kind", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, - }, - listener: gatev1.Listener{ - Name: "foo", - }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("bar"), - Name: "gateway", - Namespace: namespacePtr("default"), - Kind: kindPtr("Gateway"), - Group: groupPtr(gatev1.GroupName), - }, - }, - }, - wantAttach: false, - }, - { - desc: "Route namespace matches the Gateway namespace", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, - }, - listener: gatev1.Listener{ - Name: "foo", - }, - routeNamespace: "default", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("foo"), - Name: "gateway", - Kind: kindPtr("Gateway"), - Group: groupPtr(gatev1.GroupName), - }, - }, - }, - wantAttach: true, - wantParentRef: gatev1.ParentReference{ - SectionName: sectionNamePtr("foo"), + parentRef: gatev1.ParentReference{ + SectionName: ptr.To(gatev1.SectionName("bar")), Name: "gateway", - Kind: kindPtr("Gateway"), - Group: groupPtr(gatev1.GroupName), + Namespace: ptr.To(gatev1.Namespace("default")), + Group: ptr.To(gatev1.Group(gatev1.GroupName)), + Kind: ptr.To(gatev1.Kind("Gateway")), }, + wantMatch: false, }, { - desc: "Namespace matches the Gateway namespace", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, + desc: "Match", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", }, - listener: gatev1.Listener{ - Name: "foo", - }, - routeNamespace: "bar", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - SectionName: sectionNamePtr("foo"), - Name: "gateway", - Namespace: namespacePtr("default"), - Kind: kindPtr("Gateway"), - Group: groupPtr(gatev1.GroupName), - }, - }, - }, - wantAttach: true, - wantParentRef: gatev1.ParentReference{ - SectionName: sectionNamePtr("foo"), + parentRef: gatev1.ParentReference{ + SectionName: ptr.To(gatev1.SectionName("foo")), Name: "gateway", - Namespace: namespacePtr("default"), - Kind: kindPtr("Gateway"), - Group: groupPtr(gatev1.GroupName), + Namespace: ptr.To(gatev1.Namespace("default")), + Group: ptr.To(gatev1.Group(gatev1.GroupName)), + Kind: ptr.To(gatev1.Kind("Gateway")), }, + wantMatch: true, }, { - desc: "Only one ParentRef matches the Gateway", - gateway: &gatev1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway", - Namespace: "default", - }, + desc: "Match with route namespace defaulting", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", }, - listener: gatev1.Listener{ - Name: "foo", - }, - routeNamespace: "bar", - routeSpec: gatev1.CommonRouteSpec{ - ParentRefs: []gatev1.ParentReference{ - { - Name: "gateway2", - Namespace: namespacePtr("default"), - Kind: kindPtr("Gateway"), - Group: groupPtr(gatev1.GroupName), - }, - { - Name: "gateway", - Namespace: namespacePtr("default"), - Kind: kindPtr("Gateway"), - Group: groupPtr(gatev1.GroupName), - }, - }, - }, - wantAttach: true, - wantParentRef: gatev1.ParentReference{ - Name: "gateway", - Namespace: namespacePtr("default"), - Kind: kindPtr("Gateway"), - Group: groupPtr(gatev1.GroupName), + routeNamespace: "default", + parentRef: gatev1.ParentReference{ + SectionName: ptr.To(gatev1.SectionName("foo")), + Name: "gateway", + Group: ptr.To(gatev1.Group(gatev1.GroupName)), + Kind: ptr.To(gatev1.Kind("Gateway")), }, + wantMatch: true, }, } @@ -6276,103 +5660,104 @@ func Test_shouldAttach(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - gotParentRef, gotAttach := shouldAttach(test.gateway, test.listener, test.routeNamespace, test.routeSpec) - assert.Equal(t, test.wantAttach, gotAttach) - assert.Equal(t, test.wantParentRef, gotParentRef) + gotMatch := matchListener(test.gwListener, test.routeNamespace, test.parentRef) + assert.Equal(t, test.wantMatch, gotMatch) }) } } -func Test_matchingHostnames(t *testing.T) { +func Test_allowRoute(t *testing.T) { testCases := []struct { - desc string - listener gatev1.Listener - hostnames []gatev1.Hostname - want []gatev1.Hostname + desc string + gwListener gatewayListener + routeNamespace string + routeKind string + wantAllow bool }{ { - desc: "Empty", - }, - { - desc: "Only listener hostname", - listener: gatev1.Listener{ - Hostname: hostnamePtr("foo.com"), + desc: "Not allowed Kind", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", + AllowedRouteKinds: []string{ + "foo", + "bar", + }, }, - want: []gatev1.Hostname{"foo.com"}, + routeKind: "baz", + wantAllow: false, }, { - desc: "Only Route hostname", - hostnames: []gatev1.Hostname{"foo.com"}, - want: []gatev1.Hostname{"foo.com"}, - }, - { - desc: "Matching hostname", - listener: gatev1.Listener{ - Hostname: hostnamePtr("foo.com"), + desc: "Allowed Kind", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", + AllowedRouteKinds: []string{ + "foo", + "bar", + }, + AllowedNamespaces: []string{ + corev1.NamespaceAll, + }, }, - hostnames: []gatev1.Hostname{"foo.com"}, - want: []gatev1.Hostname{"foo.com"}, + routeKind: "bar", + wantAllow: true, }, { - desc: "Matching hostname with wildcard", - listener: gatev1.Listener{ - Hostname: hostnamePtr("*.foo.com"), + desc: "Not allowed namespace", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", + AllowedRouteKinds: []string{ + "foo", + }, + AllowedNamespaces: []string{ + "foo", + "bar", + }, }, - hostnames: []gatev1.Hostname{"*.foo.com"}, - want: []gatev1.Hostname{"*.foo.com"}, + routeKind: "foo", + routeNamespace: "baz", + wantAllow: false, }, { - desc: "Matching subdomain with listener wildcard", - listener: gatev1.Listener{ - Hostname: hostnamePtr("*.foo.com"), + desc: "Allowed namespace", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", + AllowedRouteKinds: []string{ + "foo", + }, + AllowedNamespaces: []string{ + "foo", + "bar", + }, }, - hostnames: []gatev1.Hostname{"bar.foo.com"}, - want: []gatev1.Hostname{"bar.foo.com"}, + routeKind: "foo", + routeNamespace: "foo", + wantAllow: true, }, { - desc: "Matching subdomain with route hostname wildcard", - listener: gatev1.Listener{ - Hostname: hostnamePtr("bar.foo.com"), + desc: "Allowed namespace", + gwListener: gatewayListener{ + Name: "foo", + GWName: "gateway", + GWNamespace: "default", + AllowedRouteKinds: []string{ + "foo", + }, + AllowedNamespaces: []string{ + corev1.NamespaceAll, + "bar", + }, }, - hostnames: []gatev1.Hostname{"*.foo.com"}, - want: []gatev1.Hostname{"bar.foo.com"}, - }, - { - desc: "Non matching root domain with listener wildcard", - listener: gatev1.Listener{ - Hostname: hostnamePtr("*.foo.com"), - }, - hostnames: []gatev1.Hostname{"foo.com"}, - }, - { - desc: "Non matching root domain with route hostname wildcard", - listener: gatev1.Listener{ - Hostname: hostnamePtr("foo.com"), - }, - hostnames: []gatev1.Hostname{"*.foo.com"}, - }, - { - desc: "Multiple route hostnames with one matching route hostname", - listener: gatev1.Listener{ - Hostname: hostnamePtr("*.foo.com"), - }, - hostnames: []gatev1.Hostname{"bar.com", "test.foo.com", "test.buz.com"}, - want: []gatev1.Hostname{"test.foo.com"}, - }, - { - desc: "Multiple route hostnames with non matching route hostname", - listener: gatev1.Listener{ - Hostname: hostnamePtr("*.fuz.com"), - }, - hostnames: []gatev1.Hostname{"bar.com", "test.foo.com", "test.buz.com"}, - }, - { - desc: "Multiple route hostnames with multiple matching route hostnames", - listener: gatev1.Listener{ - Hostname: hostnamePtr("*.foo.com"), - }, - hostnames: []gatev1.Hostname{"toto.foo.com", "test.foo.com", "test.buz.com"}, - want: []gatev1.Hostname{"toto.foo.com", "test.foo.com"}, + routeKind: "foo", + routeNamespace: "foo", + wantAllow: true, }, } @@ -6380,13 +5765,121 @@ func Test_matchingHostnames(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - got := matchingHostnames(test.listener, test.hostnames) + gotAllow := allowRoute(test.gwListener, test.routeNamespace, test.routeKind) + assert.Equal(t, test.wantAllow, gotAllow) + }) + } +} + +func Test_findMatchingHostnames(t *testing.T) { + testCases := []struct { + desc string + listenerHostname *gatev1.Hostname + routeHostnames []gatev1.Hostname + want []gatev1.Hostname + wantOk bool + }{ + { + desc: "Empty", + wantOk: true, + }, + { + desc: "Only listener hostname", + listenerHostname: ptr.To(gatev1.Hostname("foo.com")), + want: []gatev1.Hostname{"foo.com"}, + wantOk: true, + }, + { + desc: "Only Route hostname", + routeHostnames: []gatev1.Hostname{"foo.com"}, + want: []gatev1.Hostname{"foo.com"}, + wantOk: true, + }, + { + desc: "Matching hostname", + listenerHostname: ptr.To(gatev1.Hostname("foo.com")), + routeHostnames: []gatev1.Hostname{"foo.com"}, + want: []gatev1.Hostname{"foo.com"}, + wantOk: true, + }, + { + desc: "Matching hostname with wildcard", + listenerHostname: ptr.To(gatev1.Hostname("*.foo.com")), + routeHostnames: []gatev1.Hostname{"*.foo.com"}, + want: []gatev1.Hostname{"*.foo.com"}, + wantOk: true, + }, + { + desc: "Matching subdomain with listener wildcard", + listenerHostname: ptr.To(gatev1.Hostname("*.foo.com")), + routeHostnames: []gatev1.Hostname{"bar.foo.com"}, + want: []gatev1.Hostname{"bar.foo.com"}, + wantOk: true, + }, + { + desc: "Matching subsubdomain with listener wildcard", + listenerHostname: ptr.To(gatev1.Hostname("*.foo.com")), + routeHostnames: []gatev1.Hostname{"baz.bar.foo.com"}, + want: []gatev1.Hostname{"baz.bar.foo.com"}, + wantOk: true, + }, + { + desc: "Matching subdomain with route hostname wildcard", + listenerHostname: ptr.To(gatev1.Hostname("bar.foo.com")), + routeHostnames: []gatev1.Hostname{"*.foo.com"}, + want: []gatev1.Hostname{"bar.foo.com"}, + wantOk: true, + }, + { + desc: "Matching subsubdomain with route hostname wildcard", + listenerHostname: ptr.To(gatev1.Hostname("baz.bar.foo.com")), + routeHostnames: []gatev1.Hostname{"*.foo.com"}, + want: []gatev1.Hostname{"baz.bar.foo.com"}, + wantOk: true, + }, + { + desc: "Non matching root domain with listener wildcard", + listenerHostname: ptr.To(gatev1.Hostname("*.foo.com")), + routeHostnames: []gatev1.Hostname{"foo.com"}, + }, + { + desc: "Non matching root domain with route hostname wildcard", + listenerHostname: ptr.To(gatev1.Hostname("foo.com")), + routeHostnames: []gatev1.Hostname{"*.foo.com"}, + }, + { + desc: "Multiple route hostnames with one matching route hostname", + listenerHostname: ptr.To(gatev1.Hostname("*.foo.com")), + routeHostnames: []gatev1.Hostname{"bar.com", "test.foo.com", "test.buz.com"}, + want: []gatev1.Hostname{"test.foo.com"}, + wantOk: true, + }, + { + desc: "Multiple route hostnames with non matching route hostname", + listenerHostname: ptr.To(gatev1.Hostname("*.fuz.com")), + routeHostnames: []gatev1.Hostname{"bar.com", "test.foo.com", "test.buz.com"}, + }, + { + desc: "Multiple route hostnames with multiple matching route hostnames", + listenerHostname: ptr.To(gatev1.Hostname("*.foo.com")), + routeHostnames: []gatev1.Hostname{"toto.foo.com", "test.foo.com", "test.buz.com"}, + want: []gatev1.Hostname{"toto.foo.com", "test.foo.com"}, + wantOk: true, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got, ok := findMatchingHostnames(test.listenerHostname, test.routeHostnames) + assert.Equal(t, test.wantOk, ok) assert.Equal(t, test.want, got) }) } } -func Test_getAllowedRoutes(t *testing.T) { +func Test_allowedRouteKinds(t *testing.T) { testCases := []struct { desc string listener gatev1.Listener @@ -6400,10 +5893,10 @@ func Test_getAllowedRoutes(t *testing.T) { { desc: "Empty AllowedRoutes", supportedRouteKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, wantKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, }, { @@ -6411,12 +5904,12 @@ func Test_getAllowedRoutes(t *testing.T) { listener: gatev1.Listener{ AllowedRoutes: &gatev1.AllowedRoutes{ Kinds: []gatev1.RouteGroupKind{{ - Kind: kindTLSRoute, Group: groupPtr("foo"), + Kind: kindTLSRoute, Group: ptr.To(gatev1.Group("foo")), }}, }, }, supportedRouteKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, wantErr: true, }, @@ -6430,7 +5923,7 @@ func Test_getAllowedRoutes(t *testing.T) { }, }, supportedRouteKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, wantErr: true, }, @@ -6439,12 +5932,12 @@ func Test_getAllowedRoutes(t *testing.T) { listener: gatev1.Listener{ AllowedRoutes: &gatev1.AllowedRoutes{ Kinds: []gatev1.RouteGroupKind{{ - Kind: "foo", Group: groupPtr(gatev1.GroupName), + Kind: "foo", Group: ptr.To(gatev1.Group(gatev1.GroupName)), }}, }, }, supportedRouteKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, wantErr: true, }, @@ -6453,15 +5946,15 @@ func Test_getAllowedRoutes(t *testing.T) { listener: gatev1.Listener{ AllowedRoutes: &gatev1.AllowedRoutes{ Kinds: []gatev1.RouteGroupKind{{ - Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName), + Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName)), }}, }, }, supportedRouteKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, wantKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, }, { @@ -6469,20 +5962,20 @@ func Test_getAllowedRoutes(t *testing.T) { listener: gatev1.Listener{ AllowedRoutes: &gatev1.AllowedRoutes{ Kinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, - {Kind: kindTCPRoute, Group: groupPtr(gatev1.GroupName)}, - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, - {Kind: kindTCPRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, + {Kind: kindTCPRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, + {Kind: kindTCPRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, }, }, supportedRouteKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, - {Kind: kindTCPRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, + {Kind: kindTCPRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, wantKinds: []gatev1.RouteGroupKind{ - {Kind: kindTLSRoute, Group: groupPtr(gatev1.GroupName)}, - {Kind: kindTCPRoute, Group: groupPtr(gatev1.GroupName)}, + {Kind: kindTLSRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, + {Kind: kindTCPRoute, Group: ptr.To(gatev1.Group(gatev1.GroupName))}, }, }, } @@ -6491,7 +5984,7 @@ func Test_getAllowedRoutes(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - got, conditions := getAllowedRouteKinds(&gatev1.Gateway{}, test.listener, test.supportedRouteKinds) + got, conditions := allowedRouteKinds(&gatev1.Gateway{}, test.listener, test.supportedRouteKinds) if test.wantErr { require.NotEmpty(t, conditions, "no conditions") return @@ -6518,7 +6011,7 @@ func Test_makeListenerKey(t *testing.T) { listener: gatev1.Listener{ Port: 443, Protocol: gatev1.HTTPSProtocolType, - Hostname: hostnamePtr("www.example.com"), + Hostname: ptr.To(gatev1.Hostname("www.example.com")), }, expectedKey: "HTTPS|www.example.com|443", }, @@ -6668,7 +6161,7 @@ func Test_referenceGrantMatchesTo(t *testing.T) { { Group: "correct-group", Kind: "correct-kind", - Name: objectNamePtr("correct-name"), + Name: ptr.To(gatev1.ObjectName("correct-name")), }, }, }, @@ -6704,7 +6197,7 @@ func Test_referenceGrantMatchesTo(t *testing.T) { { Group: "", Kind: "correct-kind", - Name: objectNamePtr("correct-name"), + Name: ptr.To(gatev1.ObjectName("correct-name")), }, }, }, @@ -6722,7 +6215,7 @@ func Test_referenceGrantMatchesTo(t *testing.T) { { Group: "wrong-group", Kind: "correct-kind", - Name: objectNamePtr("correct-name"), + Name: ptr.To(gatev1.ObjectName("correct-name")), }, }, }, @@ -6740,7 +6233,7 @@ func Test_referenceGrantMatchesTo(t *testing.T) { { Group: "correct-group", Kind: "wrong-kind", - Name: objectNamePtr("correct-name"), + Name: ptr.To(gatev1.ObjectName("correct-name")), }, }, }, @@ -6758,7 +6251,7 @@ func Test_referenceGrantMatchesTo(t *testing.T) { { Group: "correct-group", Kind: "correct-kind", - Name: objectNamePtr("wrong-name"), + Name: ptr.To(gatev1.ObjectName("wrong-name")), }, }, }, @@ -6895,34 +6388,6 @@ func Test_gatewayAddresses(t *testing.T) { } } -func hostnamePtr(hostname gatev1.Hostname) *gatev1.Hostname { - return &hostname -} - -func groupPtr(group gatev1.Group) *gatev1.Group { - return &group -} - -func sectionNamePtr(sectionName gatev1.SectionName) *gatev1.SectionName { - return §ionName -} - -func namespacePtr(namespace gatev1.Namespace) *gatev1.Namespace { - return &namespace -} - -func kindPtr(kind gatev1.Kind) *gatev1.Kind { - return &kind -} - -func pathMatchTypePtr(p gatev1.PathMatchType) *gatev1.PathMatchType { return &p } - -func headerMatchTypePtr(h gatev1.HeaderMatchType) *gatev1.HeaderMatchType { return &h } - -func objectNamePtr(objectName gatev1.ObjectName) *gatev1.ObjectName { - return &objectName -} - // We cannot use the gateway-api fake.NewSimpleClientset due to Gateway being pluralized as "gatewaies" instead of "gateways". func newGatewaySimpleClientSet(t *testing.T, objects ...runtime.Object) *gatefake.Clientset { t.Helper() diff --git a/pkg/provider/kubernetes/gateway/tcproute.go b/pkg/provider/kubernetes/gateway/tcproute.go new file mode 100644 index 000000000..118ae8f1d --- /dev/null +++ b/pkg/provider/kubernetes/gateway/tcproute.go @@ -0,0 +1,296 @@ +package gateway + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/provider" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ktypes "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + gatev1 "sigs.k8s.io/gateway-api/apis/v1" + gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { + logger := log.Ctx(ctx) + routes, err := client.ListTCPRoutes() + if err != nil { + logger.Error().Err(err).Msgf("Get TCPRoutes: %s", err) + } + + for _, route := range routes { + logger := log.Ctx(ctx).With().Str("tcproute", route.Name).Str("namespace", route.Namespace).Logger() + + var parentStatuses []gatev1alpha2.RouteParentStatus + for _, parentRef := range route.Spec.ParentRefs { + parentStatus := &gatev1alpha2.RouteParentStatus{ + ParentRef: parentRef, + ControllerName: controllerName, + Conditions: []metav1.Condition{ + { + Type: string(gatev1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonAccepted), + }, + }, + } + + var attachedListeners bool + for _, listener := range gatewayListeners { + if !matchListener(listener, route.Namespace, parentRef) { + continue + } + + if !allowRoute(listener, route.Namespace, kindTCPRoute) { + continue + } + + listener.Status.AttachedRoutes++ + attachedListeners = true + + resolveConditions := p.loadTCPRoute(client, listener, route, conf) + + // TODO: handle more accurately route conditions (in case of multiple listener matching). + for _, condition := range resolveConditions { + parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition) + } + } + + if !attachedListeners { + parentStatus.Conditions = []metav1.Condition{ + { + Type: string(gatev1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonNoMatchingParent), + }, + } + } + + parentStatuses = append(parentStatuses, *parentStatus) + } + + routeStatus := gatev1alpha2.TCPRouteStatus{ + RouteStatus: gatev1alpha2.RouteStatus{ + Parents: parentStatuses, + }, + } + if err := client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil { + logger.Error(). + Err(err). + Msg("Unable to update TCPRoute status") + } + } +} + +func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *gatev1alpha2.TCPRoute, conf *dynamic.Configuration) []metav1.Condition { + routeConditions := []metav1.Condition{ + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteConditionResolvedRefs), + }, + } + + router := dynamic.TCPRouter{ + Rule: "HostSNI(`*`)", + EntryPoints: []string{listener.EPName}, + RuleSyntax: "v3", + } + + if listener.Protocol == gatev1.TLSProtocolType && listener.TLS != nil { + // TODO support let's encrypt + router.TLS = &dynamic.RouterTCPTLSConfig{ + Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough, + } + } + + // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. + routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName + routerKey := provider.Normalize(makeRouterKey("", makeID(route.Namespace, routerName))) + + var ruleServiceNames []string + for i, rule := range route.Spec.Rules { + if rule.BackendRefs == nil { + // Should not happen due to validation. + // https://github.com/kubernetes-sigs/gateway-api/blob/v0.4.0/apis/v1alpha2/tcproute_types.go#L76 + continue + } + + wrrService, subServices, err := loadTCPServices(client, route.Namespace, rule.BackendRefs) + if err != nil { + routeConditions = appendCondition(routeConditions, metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonBackendNotFound), + Message: fmt.Sprintf("Cannot load TCPRoute service %s/%s: %v", route.Namespace, route.Name, err), + }) + return routeConditions + } + + for svcName, svc := range subServices { + conf.TCP.Services[svcName] = svc + } + + serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i) + conf.TCP.Services[serviceName] = wrrService + + ruleServiceNames = append(ruleServiceNames, serviceName) + } + + if len(ruleServiceNames) == 1 { + router.Service = ruleServiceNames[0] + conf.TCP.Routers[routerKey] = &router + return routeConditions + } + + routeServiceKey := routerKey + "-wrr" + routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}} + + for _, name := range ruleServiceNames { + service := dynamic.TCPWRRService{Name: name} + service.SetDefaults() + + routeService.Weighted.Services = append(routeService.Weighted.Services, service) + } + + conf.TCP.Services[routeServiceKey] = routeService + + router.Service = routeServiceKey + conf.TCP.Routers[routerKey] = &router + + return routeConditions +} + +// loadTCPServices is generating a WRR service, even when there is only one target. +func loadTCPServices(client Client, namespace string, backendRefs []gatev1.BackendRef) (*dynamic.TCPService, map[string]*dynamic.TCPService, error) { + services := map[string]*dynamic.TCPService{} + + wrrSvc := &dynamic.TCPService{ + Weighted: &dynamic.TCPWeightedRoundRobin{ + Services: []dynamic.TCPWRRService{}, + }, + } + + for _, backendRef := range backendRefs { + if backendRef.Group == nil || backendRef.Kind == nil { + // Should not happen as this is validated by kubernetes + continue + } + + if isInternalService(backendRef) { + return nil, nil, fmt.Errorf("traefik internal service %s is not allowed in a WRR loadbalancer", backendRef.Name) + } + + weight := int(ptr.Deref(backendRef.Weight, 1)) + + if isTraefikService(backendRef) { + wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.TCPWRRService{Name: string(backendRef.Name), Weight: &weight}) + continue + } + + if *backendRef.Group != "" && *backendRef.Group != groupCore && *backendRef.Kind != "Service" { + return nil, nil, fmt.Errorf("unsupported BackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) + } + + svc := dynamic.TCPService{ + LoadBalancer: &dynamic.TCPServersLoadBalancer{}, + } + + service, exists, err := client.GetService(namespace, string(backendRef.Name)) + if err != nil { + return nil, nil, err + } + + if !exists { + return nil, nil, errors.New("service not found") + } + + if len(service.Spec.Ports) > 1 && backendRef.Port == nil { + // If the port is unspecified and the backend is a Service + // object consisting of multiple port definitions, the route + // must be dropped from the Gateway. The controller should + // raise the "ResolvedRefs" condition on the Gateway with the + // "DroppedRoutes" reason. The gateway status for this route + // should be updated with a condition that describes the error + // more specifically. + log.Error().Msg("A multiple ports Kubernetes Service cannot be used if unspecified backendRef.Port") + continue + } + + var portSpec corev1.ServicePort + var match bool + + for _, p := range service.Spec.Ports { + if backendRef.Port == nil || p.Port == int32(*backendRef.Port) { + portSpec = p + match = true + break + } + } + + if !match { + return nil, nil, errors.New("service port not found") + } + + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, string(backendRef.Name)) + if endpointsErr != nil { + return nil, nil, endpointsErr + } + + if !endpointsExists { + return nil, nil, errors.New("endpoints not found") + } + + if len(endpoints.Subsets) == 0 { + return nil, nil, errors.New("subset not found") + } + + var port int32 + var portStr string + for _, subset := range endpoints.Subsets { + for _, p := range subset.Ports { + if portSpec.Name == p.Name { + port = p.Port + break + } + } + + if port == 0 { + return nil, nil, errors.New("cannot define a port") + } + + portStr = strconv.FormatInt(int64(port), 10) + for _, addr := range subset.Addresses { + svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.TCPServer{ + Address: net.JoinHostPort(addr.IP, portStr), + }) + } + } + + serviceName := provider.Normalize(makeID(service.Namespace, service.Name) + "-" + portStr) + services[serviceName] = &svc + + wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.TCPWRRService{Name: serviceName, Weight: &weight}) + } + + if len(wrrSvc.Weighted.Services) == 0 { + return nil, nil, errors.New("no service has been created") + } + + return wrrSvc, services, nil +} diff --git a/pkg/provider/kubernetes/gateway/tlsroute.go b/pkg/provider/kubernetes/gateway/tlsroute.go new file mode 100644 index 000000000..1eb30c788 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/tlsroute.go @@ -0,0 +1,210 @@ +package gateway + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/provider" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ktypes "k8s.io/apimachinery/pkg/types" + gatev1 "sigs.k8s.io/gateway-api/apis/v1" + gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { + logger := log.Ctx(ctx) + routes, err := client.ListTLSRoutes() + if err != nil { + logger.Error().Err(err).Msgf("Get TLSRoutes: %s", err) + } + + for _, route := range routes { + logger := log.Ctx(ctx).With().Str("tlsroute", route.Name).Str("namespace", route.Namespace).Logger() + + var parentStatuses []gatev1alpha2.RouteParentStatus + for _, parentRef := range route.Spec.ParentRefs { + parentStatus := &gatev1alpha2.RouteParentStatus{ + ParentRef: parentRef, + ControllerName: controllerName, + Conditions: []metav1.Condition{ + { + Type: string(gatev1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonAccepted), + }, + }, + } + + var attachedListeners bool + for _, listener := range gatewayListeners { + if !matchListener(listener, route.Namespace, parentRef) { + continue + } + + if !allowRoute(listener, route.Namespace, kindTLSRoute) { + continue + } + + hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames) + if !ok { + continue + } + + listener.Status.AttachedRoutes++ + attachedListeners = true + + resolveConditions := p.loadTLSRoute(client, listener, route, hostnames, conf) + + // TODO: handle more accurately route conditions (in case of multiple listener matching). + for _, condition := range resolveConditions { + parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition) + } + } + + if !attachedListeners { + parentStatus.Conditions = []metav1.Condition{ + { + Type: string(gatev1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonNoMatchingParent), + }, + } + } + + parentStatuses = append(parentStatuses, *parentStatus) + } + + routeStatus := gatev1alpha2.TLSRouteStatus{ + RouteStatus: gatev1alpha2.RouteStatus{ + Parents: parentStatuses, + }, + } + if err := client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil { + logger.Error(). + Err(err). + Msg("Unable to update TLSRoute status") + } + } +} + +func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *gatev1alpha2.TLSRoute, hostnames []gatev1.Hostname, conf *dynamic.Configuration) []metav1.Condition { + routeConditions := []metav1.Condition{ + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteConditionResolvedRefs), + }, + } + + router := dynamic.TCPRouter{ + RuleSyntax: "v3", + Rule: hostSNIRule(hostnames), + EntryPoints: []string{listener.EPName}, + TLS: &dynamic.RouterTCPTLSConfig{ + Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough, + }, + } + + // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. + routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName + routerKey := provider.Normalize(makeRouterKey(router.Rule, makeID(route.Namespace, routerName))) + + var ruleServiceNames []string + for i, routeRule := range route.Spec.Rules { + if len(routeRule.BackendRefs) == 0 { + // Should not happen due to validation. + // https://github.com/kubernetes-sigs/gateway-api/blob/v0.4.0/apis/v1alpha2/tlsroute_types.go#L120 + continue + } + + wrrService, subServices, err := loadTCPServices(client, route.Namespace, routeRule.BackendRefs) + if err != nil { + // update "ResolvedRefs" status true with "InvalidBackendRefs" reason + routeConditions = appendCondition(routeConditions, metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonBackendNotFound), + Message: fmt.Sprintf("Cannot load TLSRoute service %s/%s: %v", route.Namespace, route.Name, err), + }) + continue + } + + for svcName, svc := range subServices { + conf.TCP.Services[svcName] = svc + } + + serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i) + conf.TCP.Services[serviceName] = wrrService + + ruleServiceNames = append(ruleServiceNames, serviceName) + } + + if len(ruleServiceNames) == 1 { + router.Service = ruleServiceNames[0] + conf.TCP.Routers[routerKey] = &router + + return routeConditions + } + + routeServiceKey := routerKey + "-wrr" + routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}} + + for _, name := range ruleServiceNames { + service := dynamic.TCPWRRService{Name: name} + service.SetDefaults() + + routeService.Weighted.Services = append(routeService.Weighted.Services, service) + } + + conf.TCP.Services[routeServiceKey] = routeService + + router.Service = routeServiceKey + conf.TCP.Routers[routerKey] = &router + + return routeConditions +} + +func hostSNIRule(hostnames []gatev1.Hostname) string { + rules := make([]string, 0, len(hostnames)) + uniqHostnames := map[gatev1.Hostname]struct{}{} + + for _, hostname := range hostnames { + if len(hostname) == 0 { + continue + } + + if _, exists := uniqHostnames[hostname]; exists { + continue + } + + host := string(hostname) + uniqHostnames[hostname] = struct{}{} + + wildcard := strings.Count(host, "*") + if wildcard == 0 { + rules = append(rules, fmt.Sprintf("HostSNI(`%s`)", host)) + continue + } + + host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-z0-9-\.]+\.`, 1) + rules = append(rules, fmt.Sprintf("HostSNIRegexp(`^%s$`)", host)) + } + + if len(hostnames) == 0 || len(rules) == 0 { + return "HostSNI(`*`)" + } + + return strings.Join(rules, " || ") +} diff --git a/pkg/provider/kubernetes/gateway/tlsroute_test.go b/pkg/provider/kubernetes/gateway/tlsroute_test.go new file mode 100644 index 000000000..89a688e5e --- /dev/null +++ b/pkg/provider/kubernetes/gateway/tlsroute_test.go @@ -0,0 +1,66 @@ +package gateway + +import ( + "testing" + + "github.com/stretchr/testify/assert" + gatev1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func Test_hostSNIRule(t *testing.T) { + testCases := []struct { + desc string + hostnames []gatev1.Hostname + expectedRule string + expectError bool + }{ + { + desc: "Empty", + expectedRule: "HostSNI(`*`)", + }, + { + desc: "Empty hostname", + hostnames: []gatev1.Hostname{""}, + expectedRule: "HostSNI(`*`)", + }, + { + desc: "Supported wildcard", + hostnames: []gatev1.Hostname{"*.foo"}, + expectedRule: "HostSNIRegexp(`^[a-z0-9-\\.]+\\.foo$`)", + }, + { + desc: "Some empty hostnames", + hostnames: []gatev1.Hostname{"foo", "", "bar"}, + expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)", + }, + { + desc: "Valid hostname", + hostnames: []gatev1.Hostname{"foo"}, + expectedRule: "HostSNI(`foo`)", + }, + { + desc: "Multiple valid hostnames", + hostnames: []gatev1.Hostname{"foo", "bar"}, + expectedRule: "HostSNI(`foo`) || HostSNI(`bar`)", + }, + { + desc: "Multiple valid hostnames with wildcard", + hostnames: []gatev1.Hostname{"bar.foo", "foo.foo", "*.foo"}, + expectedRule: "HostSNI(`bar.foo`) || HostSNI(`foo.foo`) || HostSNIRegexp(`^[a-z0-9-\\.]+\\.foo$`)", + }, + { + desc: "Multiple overlapping hostnames", + hostnames: []gatev1.Hostname{"foo", "bar", "foo", "baz"}, + expectedRule: "HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`)", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rule := hostSNIRule(test.hostnames) + assert.Equal(t, test.expectedRule, rule) + }) + } +}