From be00508f48bf4e267b71c21ecd2e595f735d44ac Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 28 Oct 2026 13:15:51 -0400 Subject: [PATCH 01/17] feature: Traefik serves the panel through the Gateway API instead of the retired ingress-nginx Traefik v3.7.16 is installed from charts/gateway (a wrapper around the traefik 42.1.3 chart) and charts/gateway-crds (Gateway API v1.6.3 standard + Traefik CRDs, kept on uninstall) through k3s's helm-controller. Every panel Ingress becomes an HTTPRoute on a 5stack Gateway; nginx-class Ingresses from plugins and other apps keep working through Traefik's ingress-nginx provider. update.sh hands ports 80/443 from ingress-nginx to Traefik and removes it. --- base/api/httproute-relay.yaml | 19 + base/api/httproute-ws.yaml | 19 + base/api/httproute.yaml | 173 + base/api/ingress-file-manager.yaml | 21 - base/api/ingress-relay.yaml | 20 - base/api/ingress-ws.yaml | 20 - base/api/ingress.yaml | 296 - base/api/kustomization.yaml | 7 +- base/gateway/gateway.yaml | 46 + base/gateway/ingressclass-nginx.yaml | 8 + base/{nginx => gateway}/kustomization.yaml | 3 +- base/hasura/httproute.yaml | 23 + base/hasura/ingress.yaml | 20 - base/hasura/kustomization.yaml | 2 +- base/kustomization.yaml | 2 +- base/nginx/service.yaml | 32 - base/rustfs/httproute.yaml | 13 + base/rustfs/ingress.yaml | 21 - base/rustfs/kustomization.yaml | 2 +- base/typesense/httproute.yaml | 13 + base/typesense/ingress.yaml | 18 - base/typesense/kustomization.yaml | 2 +- base/web/httproute.yaml | 13 + base/web/ingress.yaml | 20 - base/web/kustomization.yaml | 2 +- charts/gateway-crds/Chart.yaml | 7 + .../gateway-crds/templates/gateway-api.yaml | 20129 ++++++++++++++++ charts/gateway-crds/templates/traefik.yaml | 4615 ++++ charts/gateway/Chart.lock | 6 + charts/gateway/Chart.yaml | 11 + charts/gateway/charts/traefik-42.1.3.tgz | Bin 0 -> 271167 bytes charts/gateway/values.yaml | 77 + dev.sh | 3 +- fix-ssl.sh | 2 +- install.sh | 19 +- overlays/cert-manager/issuer.yaml | 9 +- overlays/config/kustomization.yaml | 4 - overlays/config/traefik-config.env.example | 4 + overlays/dev/config/kustomization.yaml | 4 - overlays/dev/config/nginx-config.env | 2 - .../config/traefik-config.env} | 0 overlays/dev/gateway-patch.yaml | 89 + overlays/dev/hasura/httproute.yaml | 13 + overlays/dev/hasura/ingress.yaml | 22 - overlays/dev/hasura/kustomization.yaml | 2 +- overlays/dev/https-redirect.yaml | 29 + overlays/dev/ingress-patch.yaml | 10 - overlays/dev/kustomization.yaml | 215 +- overlays/dev/queues/httproute.yaml | 28 + overlays/dev/queues/ingress.yaml | 24 - overlays/dev/queues/kustomization.yaml | 2 +- overlays/http/gateway-https-patch.yaml | 77 + overlays/http/http-replacements.yaml | 113 +- overlays/http/https-redirect.yaml | 27 + overlays/http/https-replacements.yaml | 232 +- overlays/http/ingress-patch.yaml | 12 - overlays/http/kustomization.yaml | 178 - overlays/mediamtx-https/ingress-patch.yaml | 8 - overlays/mediamtx-https/kustomization.yaml | 13 - overlays/mediamtx/httproute.yaml | 85 + overlays/mediamtx/ingress.yaml | 55 - overlays/mediamtx/kustomization.yaml | 21 +- overlays/mediamtx/middlewares.yaml | 66 + overlays/mediamtx/snapshot-ingress.yaml | 26 - update.sh | 55 +- utils/apply_overlay.sh | 4 +- utils/install_ingress_nginx.sh | 97 - utils/install_traefik.sh | 158 + utils/retire_ingress_nginx.sh | 46 + utils/setup-env.sh | 7 +- utils/utils.sh | 4 +- utils/write_overlays.sh | 49 + 72 files changed, 26091 insertions(+), 1383 deletions(-) create mode 100644 base/api/httproute-relay.yaml create mode 100644 base/api/httproute-ws.yaml create mode 100644 base/api/httproute.yaml delete mode 100644 base/api/ingress-file-manager.yaml delete mode 100644 base/api/ingress-relay.yaml delete mode 100644 base/api/ingress-ws.yaml delete mode 100644 base/api/ingress.yaml create mode 100644 base/gateway/gateway.yaml create mode 100644 base/gateway/ingressclass-nginx.yaml rename base/{nginx => gateway}/kustomization.yaml (62%) create mode 100644 base/hasura/httproute.yaml delete mode 100644 base/hasura/ingress.yaml delete mode 100644 base/nginx/service.yaml create mode 100644 base/rustfs/httproute.yaml delete mode 100644 base/rustfs/ingress.yaml create mode 100644 base/typesense/httproute.yaml delete mode 100644 base/typesense/ingress.yaml create mode 100644 base/web/httproute.yaml delete mode 100644 base/web/ingress.yaml create mode 100644 charts/gateway-crds/Chart.yaml create mode 100644 charts/gateway-crds/templates/gateway-api.yaml create mode 100644 charts/gateway-crds/templates/traefik.yaml create mode 100644 charts/gateway/Chart.lock create mode 100644 charts/gateway/Chart.yaml create mode 100644 charts/gateway/charts/traefik-42.1.3.tgz create mode 100644 charts/gateway/values.yaml create mode 100644 overlays/config/traefik-config.env.example delete mode 100644 overlays/dev/config/nginx-config.env rename overlays/{config/nginx-config.env.example => dev/config/traefik-config.env} (100%) create mode 100644 overlays/dev/gateway-patch.yaml create mode 100644 overlays/dev/hasura/httproute.yaml delete mode 100644 overlays/dev/hasura/ingress.yaml create mode 100644 overlays/dev/https-redirect.yaml delete mode 100644 overlays/dev/ingress-patch.yaml create mode 100644 overlays/dev/queues/httproute.yaml delete mode 100644 overlays/dev/queues/ingress.yaml create mode 100644 overlays/http/gateway-https-patch.yaml create mode 100644 overlays/http/https-redirect.yaml delete mode 100644 overlays/http/ingress-patch.yaml delete mode 100644 overlays/http/kustomization.yaml delete mode 100644 overlays/mediamtx-https/ingress-patch.yaml create mode 100644 overlays/mediamtx/httproute.yaml delete mode 100644 overlays/mediamtx/ingress.yaml create mode 100644 overlays/mediamtx/middlewares.yaml delete mode 100644 overlays/mediamtx/snapshot-ingress.yaml delete mode 100755 utils/install_ingress_nginx.sh create mode 100644 utils/install_traefik.sh create mode 100644 utils/retire_ingress_nginx.sh create mode 100644 utils/write_overlays.sh diff --git a/base/api/httproute-relay.yaml b/base/api/httproute-relay.yaml new file mode 100644 index 0000000..2d56dc9 --- /dev/null +++ b/base/api/httproute-relay.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: relay + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: relay + rules: + - filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /match-relay/ + backendRefs: + - name: api + port: 5585 diff --git a/base/api/httproute-ws.yaml b/base/api/httproute-ws.yaml new file mode 100644 index 0000000..202bd3a --- /dev/null +++ b/base/api/httproute-ws.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: ws + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: ws + rules: + - filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /ws/ + backendRefs: + - name: api + port: 5585 diff --git a/base/api/httproute.yaml b/base/api/httproute.yaml new file mode 100644 index 0000000..f3b8430 --- /dev/null +++ b/base/api/httproute.yaml @@ -0,0 +1,173 @@ +# An allowlist: a path not listed here is a 404 at the gateway and never +# reaches the api. Exact and PathPrefix are deliberate, path by path. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: api + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: api + rules: + - matches: + - path: + type: Exact + value: /me + - path: + type: PathPrefix + value: /matches + - path: + type: PathPrefix + value: /twitch + - path: + type: PathPrefix + value: /game-server-node/script + - path: + type: PathPrefix + value: /game-server-node/ping + - path: + type: PathPrefix + value: /avatars + - path: + type: PathPrefix + value: /trophies + - path: + type: Exact + value: /system/healthz + - path: + type: Exact + value: /system/logs/download + - path: + type: PathPrefix + value: /branding + - path: + type: PathPrefix + value: /news + - path: + type: PathPrefix + value: /utility + - path: + type: PathPrefix + value: /custom-pages + - path: + type: PathPrefix + value: /plugins + # Broadcast HUD bundles. The game-streamer pod fetches these + # in-cluster and never touches the gateway, but the panel's import and + # delete come from a browser, and an OBS browser source may load one + # directly -- both of which arrive here. + - path: + type: PathPrefix + value: /huds + - path: + type: PathPrefix + value: /events/media + - path: + type: PathPrefix + value: /snapshots + - path: + type: PathPrefix + value: /steam-match-history/upload + - path: + type: PathPrefix + value: /clip-views + - path: + type: PathPrefix + value: /voice + - path: + type: PathPrefix + value: /notifications + - path: + type: PathPrefix + value: /chat + # The Player Management plugin's sanction sync, which doubles as the + # heartbeat the panel reads to show it as active. + - path: + type: PathPrefix + value: /sanctions/server + - path: + type: PathPrefix + value: /file-manager + backendRefs: + - name: api + port: 5585 + - matches: + - path: + type: PathPrefix + value: /v1 + - path: + type: PathPrefix + value: /v1alpha1 + - path: + type: PathPrefix + value: /v2 + backendRefs: + - name: hasura + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: api-web + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: web + rules: + - matches: + - path: + type: PathPrefix + value: /auth + - path: + type: PathPrefix + value: /extension + - path: + type: Exact + value: /telemetry + - path: + type: Exact + value: /discord-invite + - path: + type: Exact + value: /discord-bot + - path: + type: PathPrefix + value: /calendar + - path: + type: Exact + value: /quick-connect + backendRefs: + - name: api + port: 5585 +--- +# Browsers PUT 64 MiB multipart parts to /5stack through presigned URLs, so the +# Host header and the path must reach RustFS exactly as signed: no rewrite. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: demos + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: demos + rules: + - matches: + - path: + type: PathPrefix + value: /demos + - path: + type: PathPrefix + value: /clips + backendRefs: + - name: api + port: 5585 + - matches: + - path: + type: PathPrefix + value: /5stack + backendRefs: + - name: rustfs + port: 9000 diff --git a/base/api/ingress-file-manager.yaml b/base/api/ingress-file-manager.yaml deleted file mode 100644 index 1d9fd5e..0000000 --- a/base/api/ingress-file-manager.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: file-manager - namespace: 5stack - annotations: - nginx.ingress.kubernetes.io/proxy-body-size: "4G" - nginx.ingress.kubernetes.io/client-max-body-size: "4G" -spec: - ingressClassName: nginx - rules: - - host: $(API_DOMAIN) - http: - paths: - - backend: - service: - name: api - port: - number: 5585 - path: /file-manager - pathType: Prefix \ No newline at end of file diff --git a/base/api/ingress-relay.yaml b/base/api/ingress-relay.yaml deleted file mode 100644 index 575630d..0000000 --- a/base/api/ingress-relay.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: relay - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /match-relay/$1 - namespace: 5stack -spec: - ingressClassName: nginx - rules: - - host: $(RELAY_DOMAIN) - http: - paths: - - backend: - service: - name: api - port: - number: 5585 - path: /(.*) - pathType: ImplementationSpecific \ No newline at end of file diff --git a/base/api/ingress-ws.yaml b/base/api/ingress-ws.yaml deleted file mode 100644 index 0cab6ba..0000000 --- a/base/api/ingress-ws.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ws - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /ws/$1 - namespace: 5stack -spec: - ingressClassName: nginx - rules: - - host: $(WS_DOMAIN) - http: - paths: - - backend: - service: - name: api - port: - number: 5585 - path: /(.*) - pathType: ImplementationSpecific \ No newline at end of file diff --git a/base/api/ingress.yaml b/base/api/ingress.yaml deleted file mode 100644 index 853a0e8..0000000 --- a/base/api/ingress.yaml +++ /dev/null @@ -1,296 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: api - namespace: 5stack - annotations: - nginx.ingress.kubernetes.io/proxy-body-size: "4G" - nginx.ingress.kubernetes.io/client-max-body-size: "4G" -spec: - ingressClassName: nginx - rules: - - host: $(API_DOMAIN) - http: - paths: - - backend: - service: - name: api - port: - number: 5585 - path: /me - pathType: Exact - - backend: - service: - name: api - port: - number: 5585 - path: /matches - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /twitch - pathType: Prefix - - backend: - service: - name: hasura - port: - number: 8080 - path: /v1 - pathType: Prefix - - backend: - service: - name: hasura - port: - number: 8080 - path: /v1alpha1 - pathType: Prefix - - backend: - service: - name: hasura - port: - number: 8080 - path: /v2 - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /game-server-node/script - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /game-server-node/ping - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /avatars - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /trophies - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /system/healthz - pathType: Exact - - backend: - service: - name: api - port: - number: 5585 - path: /system/logs/download - pathType: Exact - - backend: - service: - name: api - port: - number: 5585 - path: /branding - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /news - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /utility - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /custom-pages - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /plugins - pathType: Prefix - # Broadcast HUD bundles. The game-streamer pod fetches these - # in-cluster and never touches the ingress, but the panel's import and - # delete come from a browser, and an OBS browser source may load one - # directly -- both of which arrive here. - - backend: - service: - name: api - port: - number: 5585 - path: /huds - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /events/media - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /snapshots - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /steam-match-history/upload - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /clip-views - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /voice - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /notifications - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /chat - pathType: Prefix - # The Player Management plugin's sanction sync, which doubles as the - # heartbeat the panel reads to show it as active. - - backend: - service: - name: api - port: - number: 5585 - path: /sanctions/server - pathType: Prefix - - host: $(WEB_DOMAIN) - http: - paths: - - backend: - service: - name: api - port: - number: 5585 - path: /auth - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /extension - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /telemetry - pathType: Exact - - backend: - service: - name: api - port: - number: 5585 - path: /discord-invite - pathType: Exact - - backend: - service: - name: api - port: - number: 5585 - path: /discord-bot - pathType: Exact - - backend: - service: - name: api - port: - number: 5585 - path: /calendar - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /quick-connect - pathType: Exact - ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: demos - annotations: - nginx.ingress.kubernetes.io/proxy-body-size: "0" - nginx.ingress.kubernetes.io/client-max-body-size: "0" - nginx.ingress.kubernetes.io/proxy-buffering: "off" - nginx.ingress.kubernetes.io/proxy-request-buffering: "off" - nginx.ingress.kubernetes.io/chunked-transfer-encoding: "false" - nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" - namespace: 5stack -spec: - ingressClassName: nginx - rules: - - host: $(DEMOS_DOMAIN) - http: - paths: - - backend: - service: - name: api - port: - number: 5585 - path: /demos - pathType: Prefix - - backend: - service: - name: api - port: - number: 5585 - path: /clips - pathType: Prefix - - backend: - service: - name: rustfs - port: - number: 9000 - path: /5stack - pathType: Prefix \ No newline at end of file diff --git a/base/api/kustomization.yaml b/base/api/kustomization.yaml index 72ebfd2..e14786f 100644 --- a/base/api/kustomization.yaml +++ b/base/api/kustomization.yaml @@ -5,10 +5,9 @@ resources: - deployment.yaml - service.yaml - pdb.yaml - - ingress.yaml - - ingress-ws.yaml - - ingress-relay.yaml - - ingress-file-manager.yaml + - httproute.yaml + - httproute-ws.yaml + - httproute-relay.yaml - rbac/service-accounts.yaml - rbac/cluster-role-binding.yaml - rbac/cluster-role.yaml diff --git a/base/gateway/gateway.yaml b/base/gateway/gateway.yaml new file mode 100644 index 0000000..2ac2d0f --- /dev/null +++ b/base/gateway/gateway.yaml @@ -0,0 +1,46 @@ +# One listener per 5Stack host, which its routes attach to by sectionName and +# inherit the hostname from. These are the plain-HTTP listeners of a +# REVERSE_PROXY install; overlays/http/gateway-https-patch.yaml turns them into +# HTTPS ones. Ports are Traefik's entrypoints (charts/gateway), not the node's: +# HTTP_PORT and HTTPS_PORT only move the LoadBalancer Service. The listener for +# every other host is on the chart's shared Gateway, traefik/traefik-gateway. +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: 5stack + namespace: 5stack +spec: + gatewayClassName: traefik + listeners: + - name: web + hostname: ${WEB_DOMAIN} + protocol: HTTP + port: 8000 + - name: api + hostname: ${API_DOMAIN} + protocol: HTTP + port: 8000 + - name: demos + hostname: ${DEMOS_DOMAIN} + protocol: HTTP + port: 8000 + - name: ws + hostname: ${WS_DOMAIN} + protocol: HTTP + port: 8000 + - name: relay + hostname: ${RELAY_DOMAIN} + protocol: HTTP + port: 8000 + - name: typesense + hostname: ${TYPESENSE_HOST} + protocol: HTTP + port: 8000 + - name: s3-console + hostname: ${S3_CONSOLE_HOST} + protocol: HTTP + port: 8000 + - name: game-stream + hostname: ${GAME_STREAM_DOMAIN} + protocol: HTTP + port: 8000 diff --git a/base/gateway/ingressclass-nginx.yaml b/base/gateway/ingressclass-nginx.yaml new file mode 100644 index 0000000..8e5b3f1 --- /dev/null +++ b/base/gateway/ingressclass-nginx.yaml @@ -0,0 +1,8 @@ +# Served by Traefik's ingress-nginx provider, so plugins and other apps that +# still ship nginx-class Ingresses keep their routes and annotations. +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: nginx +spec: + controller: k8s.io/ingress-nginx diff --git a/base/nginx/kustomization.yaml b/base/gateway/kustomization.yaml similarity index 62% rename from base/nginx/kustomization.yaml rename to base/gateway/kustomization.yaml index a0e4bdd..cb3937d 100644 --- a/base/nginx/kustomization.yaml +++ b/base/gateway/kustomization.yaml @@ -2,4 +2,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - service.yaml \ No newline at end of file + - gateway.yaml + - ingressclass-nginx.yaml diff --git a/base/hasura/httproute.yaml b/base/hasura/httproute.yaml new file mode 100644 index 0000000..19549d6 --- /dev/null +++ b/base/hasura/httproute.yaml @@ -0,0 +1,23 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: hasura + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: api + rules: + - matches: + - path: + type: Exact + value: /v2/healthz + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplaceFullPath + replaceFullPath: /healthz + backendRefs: + - name: hasura + port: 8080 diff --git a/base/hasura/ingress.yaml b/base/hasura/ingress.yaml deleted file mode 100644 index c69713e..0000000 --- a/base/hasura/ingress.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: hasura - namespace: 5stack - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /healthz -spec: - ingressClassName: nginx - rules: - - host: $(API_DOMAIN) - http: - paths: - - backend: - service: - name: hasura - port: - number: 8080 - path: /v2/healthz - pathType: Exact diff --git a/base/hasura/kustomization.yaml b/base/hasura/kustomization.yaml index 78e44d0..95d9a61 100644 --- a/base/hasura/kustomization.yaml +++ b/base/hasura/kustomization.yaml @@ -4,5 +4,5 @@ kind: Kustomization resources: - deployment.yaml - service.yaml - - ingress.yaml + - httproute.yaml - pdb.yaml \ No newline at end of file diff --git a/base/kustomization.yaml b/base/kustomization.yaml index b197629..95eb368 100644 --- a/base/kustomization.yaml +++ b/base/kustomization.yaml @@ -13,5 +13,5 @@ resources: - typesense - web - volumes - - nginx + - gateway - backups \ No newline at end of file diff --git a/base/nginx/service.yaml b/base/nginx/service.yaml deleted file mode 100644 index 50fb3c7..0000000 --- a/base/nginx/service.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/version: 1.12.1 - name: ingress-nginx-controller - namespace: ingress-nginx -spec: - externalTrafficPolicy: Local - ipFamilies: - - IPv4 - ipFamilyPolicy: SingleStack - ports: - - appProtocol: http - name: http - port: 80 - protocol: TCP - targetPort: http - - appProtocol: https - name: https - port: 443 - protocol: TCP - targetPort: https - selector: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: ingress-nginx - app.kubernetes.io/name: ingress-nginx - type: LoadBalancer diff --git a/base/rustfs/httproute.yaml b/base/rustfs/httproute.yaml new file mode 100644 index 0000000..24aed8c --- /dev/null +++ b/base/rustfs/httproute.yaml @@ -0,0 +1,13 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: s3-console + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: s3-console + rules: + - backendRefs: + - name: rustfs + port: 9001 diff --git a/base/rustfs/ingress.yaml b/base/rustfs/ingress.yaml deleted file mode 100644 index 387d581..0000000 --- a/base/rustfs/ingress.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /$1 - nginx.ingress.kubernetes.io/proxy-body-size: 2048m - name: s3-console - namespace: 5stack -spec: - ingressClassName: nginx - rules: - - host: ${S3_CONSOLE_HOST} - http: - paths: - - backend: - service: - name: rustfs - port: - number: 9001 - path: /(.*) - pathType: ImplementationSpecific diff --git a/base/rustfs/kustomization.yaml b/base/rustfs/kustomization.yaml index 4877246..b3d6492 100644 --- a/base/rustfs/kustomization.yaml +++ b/base/rustfs/kustomization.yaml @@ -4,4 +4,4 @@ kind: Kustomization resources: - stateful-set.yaml - service.yaml - - ingress.yaml \ No newline at end of file + - httproute.yaml \ No newline at end of file diff --git a/base/typesense/httproute.yaml b/base/typesense/httproute.yaml new file mode 100644 index 0000000..e8e893f --- /dev/null +++ b/base/typesense/httproute.yaml @@ -0,0 +1,13 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: typesense + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: typesense + rules: + - backendRefs: + - name: typesense + port: 8108 diff --git a/base/typesense/ingress.yaml b/base/typesense/ingress.yaml deleted file mode 100644 index d3a57bb..0000000 --- a/base/typesense/ingress.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: typesense - namespace: 5stack -spec: - ingressClassName: nginx - rules: - - host: $(TYPESENSE_HOST) - http: - paths: - - backend: - service: - name: typesense - port: - number: 8108 - path: / - pathType: Prefix \ No newline at end of file diff --git a/base/typesense/kustomization.yaml b/base/typesense/kustomization.yaml index b6997f9..a0fae09 100644 --- a/base/typesense/kustomization.yaml +++ b/base/typesense/kustomization.yaml @@ -4,5 +4,5 @@ kind: Kustomization resources: - stateful-set.yaml - service.yaml - - ingress.yaml + - httproute.yaml - configmap.yaml \ No newline at end of file diff --git a/base/web/httproute.yaml b/base/web/httproute.yaml new file mode 100644 index 0000000..f3ee210 --- /dev/null +++ b/base/web/httproute.yaml @@ -0,0 +1,13 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: web + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: web + rules: + - backendRefs: + - name: web + port: 3000 diff --git a/base/web/ingress.yaml b/base/web/ingress.yaml deleted file mode 100644 index 5dc44a4..0000000 --- a/base/web/ingress.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /$1 - name: web - namespace: 5stack -spec: - ingressClassName: nginx - rules: - - host: ${WEB_DOMAIN} - http: - paths: - - backend: - service: - name: web - port: - number: 3000 - path: /(.*) - pathType: ImplementationSpecific \ No newline at end of file diff --git a/base/web/kustomization.yaml b/base/web/kustomization.yaml index 350ec31..572cdd7 100644 --- a/base/web/kustomization.yaml +++ b/base/web/kustomization.yaml @@ -4,4 +4,4 @@ kind: Kustomization resources: - deployment.yaml - service.yaml - - ingress.yaml \ No newline at end of file + - httproute.yaml \ No newline at end of file diff --git a/charts/gateway-crds/Chart.yaml b/charts/gateway-crds/Chart.yaml new file mode 100644 index 0000000..984e129 --- /dev/null +++ b/charts/gateway-crds/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: gateway-crds +description: The Gateway API (v1.6.3, standard channel) and Traefik v3.7 CRDs that the gateway chart needs. Install it first. +type: application +version: 0.1.0 +appVersion: v1.6.3 +kubeVersion: ">=1.31.0-0" diff --git a/charts/gateway-crds/templates/gateway-api.yaml b/charts/gateway-crds/templates/gateway-api.yaml new file mode 100644 index 0000000..37d14c1 --- /dev/null +++ b/charts/gateway-crds/templates/gateway-api.yaml @@ -0,0 +1,20129 @@ +# Gateway API v1.6.3, standard channel: https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml +# (sha256 356c2b8e286a964dc20995435659ad1095386329974793af10bbfe3eff977fe5), with helm.sh/resource-policy: keep +# added to each CRD so an uninstall never takes every HTTPRoute on the cluster with it. +# Copyright The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Gateway API Standard channel install +# +--- +# +# config/crd/standard/gateway.networking.k8s.io_backendtlspolicies.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + labels: + gateway.networking.k8s.io/policy: Direct + name: backendtlspolicies.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: BackendTLSPolicy + listKind: BackendTLSPolicyList + plural: backendtlspolicies + shortNames: + - btlspolicy + singular: backendtlspolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + BackendTLSPolicy provides a way to configure how a Gateway + connects to a Backend via TLS. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of BackendTLSPolicy. + properties: + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + targetRefs: + description: |- + TargetRefs identifies an API object to apply the policy to. + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + + TargetRefs must be _distinct_. This means either that: + + * They select different targets. If this is the case, then targetRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, and `name` must + be unique across all targetRef entries in the BackendTLSPolicy. + * They select different sectionNames in the same target. + + When more than one BackendTLSPolicy selects the same target and + sectionName, implementations MUST determine precedence using the + following criteria, continuing on ties: + + * The older policy by creation timestamp takes precedence. For + example, a policy with a creation timestamp of "2021-07-15 + 01:02:03" MUST be given precedence over a policy with a + creation timestamp of "2021-07-15 01:02:04". + * The policy appearing first in alphabetical order by {namespace}/{name}. + For example, a policy named `foo/bar` is given precedence over a + policy named `foo/baz`. + + For any BackendTLSPolicy that does not take precedence, the + implementation MUST ensure the `Accepted` Condition is set to + `status: False`, with Reason `Conflicted`. + + Implementations SHOULD NOT support more than one targetRef at this + time. Although the API technically allows for this, the current guidance + for conflict resolution and status handling is lacking. Until that can be + clarified in a future release, the safest approach is to support a single + targetRef. + + Support Levels: + + * Extended: Kubernetes Service referenced by backendRefs used on a Route. + - HTTPRoute, GRPCRoute, TLSRoute with termination + - Filters that needs a backend of type Service, like Mirror and External Authorization + + * Implementation-Specific: Implementations MAY use BackendTLSPolicy for: + - Services not referenced by any Route (e.g., infrastructure services) + - Service mesh workload-to-service communication + - Other resource types beyond Service + + Implementations SHOULD aim to ensure that BackendTLSPolicy behavior is consistent, + even outside of the extended HTTPRoute -(backendRef) -> Service path. + They SHOULD clearly document how BackendTLSPolicy is interpreted in these + scenarios, including: + - Which resources beyond Service are supported + - How the policy is discovered and applied + - Any implementation-specific semantics or restrictions + + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + items: + description: |- + LocalPolicyTargetReferenceWithSectionName identifies an API object to apply a + direct policy to. This should be used as part of Policy resources that can + target single resources. For more information on how this policy attachment + mode works, and a sample Policy resource, refer to the policy attachment + documentation for Gateway API. + + Note: This should only be used for direct policy attachment when references + to SectionName are actually needed. In all other cases, + LocalPolicyTargetReference should be used. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when targetRefs includes + 2 or more references to the same target + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name ? ((!has(p1.sectionName) || p1.sectionName + == '''') == (!has(p2.sectionName) || p2.sectionName == '''')) + : true))' + - message: sectionName must be unique when targetRefs includes 2 or + more references to the same target + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.sectionName) || + p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + validation: + description: Validation contains backend TLS validation configuration. + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes objects that + contain a PEM-encoded TLS CA certificate bundle, which is used to + validate a TLS handshake between the Gateway and backend Pod. + + If CACertificateRefs is empty or unspecified, then WellKnownCACertificates must be + specified. Only one of CACertificateRefs or WellKnownCACertificates may be specified, + not both. If CACertificateRefs is empty or unspecified, the configuration for + WellKnownCACertificates MUST be honored instead if supported by the implementation. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a ConfigMap does not contain a key + named `ca.crt`). In this case, the Reason must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this case, the Reason + must be set to `InvalidKind` and the Message of the Condition must explain which + kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace. This may change in future + spec updates. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message must be set for the invalid reference. + + In all cases, the implementation MUST ensure the `ResolvedRefs` Condition on + the BackendTLSPolicy is set to `status: False`, with a Reason and Message + that indicate the cause of the error. Connections using an invalid + CACertificateRef MUST fail, and the client MUST receive an HTTP 5xx error + response. If ALL CACertificateRefs are invalid, the implementation MUST also + ensure the `Accepted` Condition on the BackendTLSPolicy is set to + `status: False`, with a Reason `NoValidCACertificate`. + + A single CACertificateRef to a Kubernetes ConfigMap kind has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a backend, but this behavior is implementation-specific. + + Support: Core - An optional single reference to a Kubernetes ConfigMap, + with the CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + LocalObjectReference identifies an API object within the namespace of the + referrer. + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example "HTTPRoute" + or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + hostname: + description: |- + Hostname is used for two purposes in the connection between Gateways and + backends: + + 1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066). + 2. Hostname MUST be used for authentication and MUST match the certificate + served by the matching backend, unless SubjectAltNames is specified. + 3. If SubjectAltNames are specified, Hostname can be used for certificate selection + but MUST NOT be used for authentication. If you want to use the value + of the Hostname field for authentication, you MUST add it to the SubjectAltNames list. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + subjectAltNames: + description: |- + SubjectAltNames contains one or more Subject Alternative Names. + When specified the certificate served from the backend MUST + have at least one Subject Alternate Name matching one of the specified SubjectAltNames. + + Support: Extended + items: + description: SubjectAltName represents Subject Alternative Name. + properties: + hostname: + description: |- + Hostname contains Subject Alternative Name specified in DNS name format. + Required when Type is set to Hostname, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: + description: |- + Type determines the format of the Subject Alternative Name. Always required. + + Support: Core + enum: + - Hostname + - URI + type: string + uri: + description: |- + URI contains Subject Alternative Name specified in a full URI format. + It MUST include both a scheme (e.g., "http" or "ftp") and a scheme-specific-part. + Common values include SPIFFE IDs like "spiffe://mycluster.example.com/ns/myns/sa/svc1sa". + Required when Type is set to URI, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\?([^#]*))?(#(.*))? + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: SubjectAltName element must contain Hostname, if + Type is set to Hostname + rule: '!(self.type == "Hostname" && (!has(self.hostname) || + self.hostname == ""))' + - message: SubjectAltName element must not contain Hostname, + if Type is not set to Hostname + rule: '!(self.type != "Hostname" && has(self.hostname) && + self.hostname != "")' + - message: SubjectAltName element must contain URI, if Type + is set to URI + rule: '!(self.type == "URI" && (!has(self.uri) || self.uri + == ""))' + - message: SubjectAltName element must not contain URI, if Type + is not set to URI + rule: '!(self.type != "URI" && has(self.uri) && self.uri != + "")' + maxItems: 5 + type: array + x-kubernetes-list-type: atomic + wellKnownCACertificates: + description: |- + WellKnownCACertificates specifies whether a well-known set of CA certificates + may be used in the TLS handshake between the gateway and backend pod. + + If WellKnownCACertificates is unspecified or empty (""), then CACertificateRefs + must be specified with at least one entry for a valid configuration. Only one of + CACertificateRefs or WellKnownCACertificates may be specified, not both. + If an implementation does not support the WellKnownCACertificates field, or + the supplied value is not recognized, the implementation MUST ensure the + `Accepted` Condition on the BackendTLSPolicy is set to `status: False`, with + a Reason `Invalid`. + + Valid values include: + * "System" - indicates that well-known system CA certificates should be used. + + Implementations MAY define their own sets of CA certificates. Such definitions + MUST use an implementation-specific, prefixed name, such as + `mycompany.com/my-custom-ca-certificates`. + + Support: Implementation-specific + maxLength: 253 + minLength: 1 + pattern: ^(System|([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]))$ + type: string + required: + - hostname + type: object + x-kubernetes-validations: + - message: must not contain both CACertificateRefs and WellKnownCACertificates + rule: '!(has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 && has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "")' + - message: must specify either CACertificateRefs or WellKnownCACertificates + rule: (has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 || has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "") + required: + - targetRefs + - validation + type: object + status: + description: Status defines the current state of BackendTLSPolicy. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - conditions + - controllerName + type: object + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + required: + - ancestors + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - deprecated: true + deprecationWarning: The v1alpha3 version of BackendTLSPolicy has been deprecated + and will be removed in a future release of the API. Please upgrade to v1. + name: v1alpha3 + schema: + openAPIV3Schema: + description: |- + BackendTLSPolicy provides a way to configure how a Gateway + connects to a Backend via TLS. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of BackendTLSPolicy. + properties: + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + targetRefs: + description: |- + TargetRefs identifies an API object to apply the policy to. + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + + TargetRefs must be _distinct_. This means either that: + + * They select different targets. If this is the case, then targetRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, and `name` must + be unique across all targetRef entries in the BackendTLSPolicy. + * They select different sectionNames in the same target. + + When more than one BackendTLSPolicy selects the same target and + sectionName, implementations MUST determine precedence using the + following criteria, continuing on ties: + + * The older policy by creation timestamp takes precedence. For + example, a policy with a creation timestamp of "2021-07-15 + 01:02:03" MUST be given precedence over a policy with a + creation timestamp of "2021-07-15 01:02:04". + * The policy appearing first in alphabetical order by {namespace}/{name}. + For example, a policy named `foo/bar` is given precedence over a + policy named `foo/baz`. + + For any BackendTLSPolicy that does not take precedence, the + implementation MUST ensure the `Accepted` Condition is set to + `status: False`, with Reason `Conflicted`. + + Implementations SHOULD NOT support more than one targetRef at this + time. Although the API technically allows for this, the current guidance + for conflict resolution and status handling is lacking. Until that can be + clarified in a future release, the safest approach is to support a single + targetRef. + + Support Levels: + + * Extended: Kubernetes Service referenced by backendRefs used on a Route. + - HTTPRoute, GRPCRoute, TLSRoute with termination + - Filters that needs a backend of type Service, like Mirror and External Authorization + + * Implementation-Specific: Implementations MAY use BackendTLSPolicy for: + - Services not referenced by any Route (e.g., infrastructure services) + - Service mesh workload-to-service communication + - Other resource types beyond Service + + Implementations SHOULD aim to ensure that BackendTLSPolicy behavior is consistent, + even outside of the extended HTTPRoute -(backendRef) -> Service path. + They SHOULD clearly document how BackendTLSPolicy is interpreted in these + scenarios, including: + - Which resources beyond Service are supported + - How the policy is discovered and applied + - Any implementation-specific semantics or restrictions + + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + items: + description: |- + LocalPolicyTargetReferenceWithSectionName identifies an API object to apply a + direct policy to. This should be used as part of Policy resources that can + target single resources. For more information on how this policy attachment + mode works, and a sample Policy resource, refer to the policy attachment + documentation for Gateway API. + + Note: This should only be used for direct policy attachment when references + to SectionName are actually needed. In all other cases, + LocalPolicyTargetReference should be used. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when targetRefs includes + 2 or more references to the same target + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name ? ((!has(p1.sectionName) || p1.sectionName + == '''') == (!has(p2.sectionName) || p2.sectionName == '''')) + : true))' + - message: sectionName must be unique when targetRefs includes 2 or + more references to the same target + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.sectionName) || + p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + validation: + description: Validation contains backend TLS validation configuration. + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes objects that + contain a PEM-encoded TLS CA certificate bundle, which is used to + validate a TLS handshake between the Gateway and backend Pod. + + If CACertificateRefs is empty or unspecified, then WellKnownCACertificates must be + specified. Only one of CACertificateRefs or WellKnownCACertificates may be specified, + not both. If CACertificateRefs is empty or unspecified, the configuration for + WellKnownCACertificates MUST be honored instead if supported by the implementation. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a ConfigMap does not contain a key + named `ca.crt`). In this case, the Reason must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this case, the Reason + must be set to `InvalidKind` and the Message of the Condition must explain which + kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace. This may change in future + spec updates. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message must be set for the invalid reference. + + In all cases, the implementation MUST ensure the `ResolvedRefs` Condition on + the BackendTLSPolicy is set to `status: False`, with a Reason and Message + that indicate the cause of the error. Connections using an invalid + CACertificateRef MUST fail, and the client MUST receive an HTTP 5xx error + response. If ALL CACertificateRefs are invalid, the implementation MUST also + ensure the `Accepted` Condition on the BackendTLSPolicy is set to + `status: False`, with a Reason `NoValidCACertificate`. + + A single CACertificateRef to a Kubernetes ConfigMap kind has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a backend, but this behavior is implementation-specific. + + Support: Core - An optional single reference to a Kubernetes ConfigMap, + with the CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + LocalObjectReference identifies an API object within the namespace of the + referrer. + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example "HTTPRoute" + or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + hostname: + description: |- + Hostname is used for two purposes in the connection between Gateways and + backends: + + 1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066). + 2. Hostname MUST be used for authentication and MUST match the certificate + served by the matching backend, unless SubjectAltNames is specified. + 3. If SubjectAltNames are specified, Hostname can be used for certificate selection + but MUST NOT be used for authentication. If you want to use the value + of the Hostname field for authentication, you MUST add it to the SubjectAltNames list. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + subjectAltNames: + description: |- + SubjectAltNames contains one or more Subject Alternative Names. + When specified the certificate served from the backend MUST + have at least one Subject Alternate Name matching one of the specified SubjectAltNames. + + Support: Extended + items: + description: SubjectAltName represents Subject Alternative Name. + properties: + hostname: + description: |- + Hostname contains Subject Alternative Name specified in DNS name format. + Required when Type is set to Hostname, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: + description: |- + Type determines the format of the Subject Alternative Name. Always required. + + Support: Core + enum: + - Hostname + - URI + type: string + uri: + description: |- + URI contains Subject Alternative Name specified in a full URI format. + It MUST include both a scheme (e.g., "http" or "ftp") and a scheme-specific-part. + Common values include SPIFFE IDs like "spiffe://mycluster.example.com/ns/myns/sa/svc1sa". + Required when Type is set to URI, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\?([^#]*))?(#(.*))? + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: SubjectAltName element must contain Hostname, if + Type is set to Hostname + rule: '!(self.type == "Hostname" && (!has(self.hostname) || + self.hostname == ""))' + - message: SubjectAltName element must not contain Hostname, + if Type is not set to Hostname + rule: '!(self.type != "Hostname" && has(self.hostname) && + self.hostname != "")' + - message: SubjectAltName element must contain URI, if Type + is set to URI + rule: '!(self.type == "URI" && (!has(self.uri) || self.uri + == ""))' + - message: SubjectAltName element must not contain URI, if Type + is not set to URI + rule: '!(self.type != "URI" && has(self.uri) && self.uri != + "")' + maxItems: 5 + type: array + x-kubernetes-list-type: atomic + wellKnownCACertificates: + description: |- + WellKnownCACertificates specifies whether a well-known set of CA certificates + may be used in the TLS handshake between the gateway and backend pod. + + If WellKnownCACertificates is unspecified or empty (""), then CACertificateRefs + must be specified with at least one entry for a valid configuration. Only one of + CACertificateRefs or WellKnownCACertificates may be specified, not both. + If an implementation does not support the WellKnownCACertificates field, or + the supplied value is not recognized, the implementation MUST ensure the + `Accepted` Condition on the BackendTLSPolicy is set to `status: False`, with + a Reason `Invalid`. + + Valid values include: + * "System" - indicates that well-known system CA certificates should be used. + + Implementations MAY define their own sets of CA certificates. Such definitions + MUST use an implementation-specific, prefixed name, such as + `mycompany.com/my-custom-ca-certificates`. + + Support: Implementation-specific + maxLength: 253 + minLength: 1 + pattern: ^(System|([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]))$ + type: string + required: + - hostname + type: object + x-kubernetes-validations: + - message: must not contain both CACertificateRefs and WellKnownCACertificates + rule: '!(has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 && has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "")' + - message: must specify either CACertificateRefs or WellKnownCACertificates + rule: (has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 || has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "") + required: + - targetRefs + - validation + type: object + status: + description: Status defines the current state of BackendTLSPolicy. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - conditions + - controllerName + type: object + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + required: + - ancestors + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_gatewayclasses.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: gatewayclasses.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: GatewayClass + listKind: GatewayClassList + plural: gatewayclasses + shortNames: + - gc + singular: gatewayclass + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.controllerName + name: Controller + type: string + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .spec.description + name: Description + priority: 1 + type: string + name: v1 + schema: + openAPIV3Schema: + description: |- + GatewayClass describes a class of Gateways available to the user for creating + Gateway resources. + + It is recommended that this resource be used as a template for Gateways. This + means that a Gateway is based on the state of the GatewayClass at the time it + was created and changes to the GatewayClass or associated parameters are not + propagated down to existing Gateways. This recommendation is intended to + limit the blast radius of changes to GatewayClass or associated parameters. + If implementations choose to propagate GatewayClass changes to existing + Gateways, that MUST be clearly documented by the implementation. + + Whenever one or more Gateways are using a GatewayClass, implementations SHOULD + add the `gateway-exists-finalizer.gateway.networking.k8s.io` finalizer on the + associated GatewayClass. This ensures that a GatewayClass associated with a + Gateway is not deleted while in use. + + GatewayClass is a Cluster level resource. + + A GatewayClass name SHOULD be compliant with RFC 1035, consisting of a maximum of 63 lower case alphanumeric + characters or hyphens ('-'), and MUST start and end with an alphanumeric character. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of GatewayClass. + properties: + controllerName: + description: |- + ControllerName is the name of the controller that is managing Gateways of + this class. The value of this field MUST be a domain prefixed path. + + Example: "example.net/gateway-controller". + + This field is not mutable and cannot be empty. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + description: + description: Description helps describe a GatewayClass with more details. + maxLength: 64 + type: string + parametersRef: + description: |- + ParametersRef is a reference to a resource that contains the configuration + parameters corresponding to the GatewayClass. This is optional if the + controller does not require any additional configuration. + + ParametersRef can reference a standard Kubernetes resource, i.e. ConfigMap, + or an implementation-specific custom resource. The resource can be + cluster-scoped or namespace-scoped. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the GatewayClass SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + A Gateway for this GatewayClass may provide its own `parametersRef`. When both are specified, + the merging behavior is implementation specific. + It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + This field is required when referring to a Namespace-scoped resource and + MUST be unset when referring to a Cluster-scoped resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + required: + - controllerName + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: |- + Status defines the current state of GatewayClass. + + Implementations MUST populate status on all GatewayClass resources which + specify their controller name. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: |- + Conditions is the current status from the controller for + this GatewayClass. + + Controllers should prefer to publish conditions using values + of GatewayClassConditionType for the type of each Condition. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + supportedFeatures: + description: |- + SupportedFeatures is the set of features the GatewayClass support. + It MUST be sorted in ascending alphabetical order by the Name key. + items: + properties: + name: + description: |- + FeatureName is used to describe distinct features that are covered by + conformance tests. + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.controllerName + name: Controller + type: string + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .spec.description + name: Description + priority: 1 + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + GatewayClass describes a class of Gateways available to the user for creating + Gateway resources. + + It is recommended that this resource be used as a template for Gateways. This + means that a Gateway is based on the state of the GatewayClass at the time it + was created and changes to the GatewayClass or associated parameters are not + propagated down to existing Gateways. This recommendation is intended to + limit the blast radius of changes to GatewayClass or associated parameters. + If implementations choose to propagate GatewayClass changes to existing + Gateways, that MUST be clearly documented by the implementation. + + Whenever one or more Gateways are using a GatewayClass, implementations SHOULD + add the `gateway-exists-finalizer.gateway.networking.k8s.io` finalizer on the + associated GatewayClass. This ensures that a GatewayClass associated with a + Gateway is not deleted while in use. + + GatewayClass is a Cluster level resource. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of GatewayClass. + properties: + controllerName: + description: |- + ControllerName is the name of the controller that is managing Gateways of + this class. The value of this field MUST be a domain prefixed path. + + Example: "example.net/gateway-controller". + + This field is not mutable and cannot be empty. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + description: + description: Description helps describe a GatewayClass with more details. + maxLength: 64 + type: string + parametersRef: + description: |- + ParametersRef is a reference to a resource that contains the configuration + parameters corresponding to the GatewayClass. This is optional if the + controller does not require any additional configuration. + + ParametersRef can reference a standard Kubernetes resource, i.e. ConfigMap, + or an implementation-specific custom resource. The resource can be + cluster-scoped or namespace-scoped. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the GatewayClass SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + A Gateway for this GatewayClass may provide its own `parametersRef`. When both are specified, + the merging behavior is implementation specific. + It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + This field is required when referring to a Namespace-scoped resource and + MUST be unset when referring to a Cluster-scoped resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + required: + - controllerName + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: |- + Status defines the current state of GatewayClass. + + Implementations MUST populate status on all GatewayClass resources which + specify their controller name. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: |- + Conditions is the current status from the controller for + this GatewayClass. + + Controllers should prefer to publish conditions using values + of GatewayClassConditionType for the type of each Condition. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + supportedFeatures: + description: |- + SupportedFeatures is the set of features the GatewayClass support. + It MUST be sorted in ascending alphabetical order by the Name key. + items: + properties: + name: + description: |- + FeatureName is used to describe distinct features that are covered by + conformance tests. + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_gateways.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: gateways.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: Gateway + listKind: GatewayList + plural: gateways + shortNames: + - gtw + singular: gateway + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.gatewayClassName + name: Class + type: string + - jsonPath: .status.addresses[*].value + name: Address + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + Gateway represents an instance of a service-traffic handling infrastructure + by binding Listeners to a set of IP addresses. + A Gateway name SHOULD be compliant with RFC 1035, consisting of a maximum of 63 lower case alphanumeric + characters or hyphens ('-'), and MUST start and end with an alphanumeric character. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of Gateway. + properties: + addresses: + description: |- + Addresses requested for this Gateway. This is optional and behavior can + depend on the implementation. If a value is set in the spec and the + requested address is invalid or unavailable, the implementation MUST + indicate this in an associated entry in GatewayStatus.Conditions. + + The Addresses field represents a request for the address(es) on the + "outside of the Gateway", that traffic bound for this Gateway will use. + This could be the IP address or hostname of an external load balancer or + other networking infrastructure, or some other address that traffic will + be sent to. + + If no Addresses are specified, the implementation MAY schedule the + Gateway in an implementation-specific manner, assigning an appropriate + set of Addresses. + + The implementation MUST bind all Listeners to every GatewayAddress that + it assigns to the Gateway and add a corresponding entry in + GatewayStatus.Addresses. + + Support: Extended + items: + description: GatewaySpecAddress describes an address that can be + bound to a Gateway. + oneOf: + - properties: + type: + enum: + - IPAddress + value: + anyOf: + - format: ipv4 + - format: ipv6 + - properties: + type: + not: + enum: + - IPAddress + properties: + type: + default: IPAddress + description: Type of the address. + maxLength: 253 + minLength: 1 + pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + value: + description: |- + When a value is unspecified, an implementation SHOULD automatically + assign an address matching the requested type if possible. + + If an implementation does not support an empty value, they MUST set the + "Programmed" condition in status to False with a reason of "AddressNotAssigned". + + Examples: `1.2.3.4`, `128::1`, `my-ip-address`. + maxLength: 253 + type: string + type: object + x-kubernetes-validations: + - message: Hostname value must be empty or contain only valid characters + (matching ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? (!has(self.value) || self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$""")): + true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: IPAddress values must be unique + rule: 'self.all(a1, a1.type == ''IPAddress'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + - message: Hostname values must be unique + rule: 'self.all(a1, a1.type == ''Hostname'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + allowedListeners: + description: |- + AllowedListeners defines which ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. + properties: + namespaces: + default: + from: None + description: |- + Namespaces defines which namespaces ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. + properties: + from: + default: None + description: |- + From indicates where ListenerSets can attach to this Gateway. Possible + values are: + + * Same: Only ListenerSets in the same namespace may be attached to this Gateway. + * Selector: ListenerSets in namespaces selected by the selector may be attached to this Gateway. + * All: ListenerSets in all namespaces may be attached to this Gateway. + * None: Only listeners defined in the Gateway's spec are allowed + + The default value None + enum: + - All + - Selector + - Same + - None + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only ListenerSets in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + gatewayClassName: + description: |- + GatewayClassName used for this Gateway. This is the name of a + GatewayClass resource. + maxLength: 253 + minLength: 1 + type: string + infrastructure: + description: |- + Infrastructure defines infrastructure level attributes about this Gateway instance. + + Support: Extended + properties: + annotations: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Annotations that SHOULD be applied to any resources created in response to this Gateway. + + For implementations creating other Kubernetes objects, this should be the `metadata.annotations` field on resources. + For other implementations, this refers to any relevant (implementation specific) "annotations" concepts. + + An implementation may chose to add additional implementation-specific annotations as they see fit. + + Support: Extended + maxProperties: 16 + type: object + x-kubernetes-validations: + - message: Annotation keys must be in the form of an optional + DNS subdomain prefix followed by a required name segment of + up to 63 characters. + rule: self.all(key, key.matches(r"""^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]$""")) + - message: If specified, the annotation key's prefix must be a + DNS subdomain not longer than 253 characters in total. + rule: self.all(key, key.split("/")[0].size() < 253) + labels: + additionalProperties: + description: |- + LabelValue is the value of a label in the Gateway API. This is used for validation + of maps such as Gateway infrastructure labels. This matches the Kubernetes + label validation rules: + * must be 63 characters or less (can be empty), + * unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]), + * could contain dashes (-), underscores (_), dots (.), and alphanumerics between. + + Valid values include: + + * MyValue + * my.name + * 123-my-value + maxLength: 63 + minLength: 0 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + description: |- + Labels that SHOULD be applied to any resources created in response to this Gateway. + + For implementations creating other Kubernetes objects, this should be the `metadata.labels` field on resources. + For other implementations, this refers to any relevant (implementation specific) "labels" concepts. + + An implementation may chose to add additional implementation-specific labels as they see fit. + + If an implementation maps these labels to Pods, or any other resource that would need to be recreated when labels + change, it SHOULD clearly warn about this behavior in documentation. + + Support: Extended + maxProperties: 8 + type: object + x-kubernetes-validations: + - message: Label keys must be in the form of an optional DNS subdomain + prefix followed by a required name segment of up to 63 characters. + rule: self.all(key, key.matches(r"""^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]$""")) + - message: If specified, the label key's prefix must be a DNS + subdomain not longer than 253 characters in total. + rule: self.all(key, key.split("/")[0].size() < 253) + parametersRef: + description: |- + ParametersRef is a reference to a resource that contains the configuration + parameters corresponding to the Gateway. This is optional if the + controller does not require any additional configuration. + + This follows the same semantics as GatewayClass's `parametersRef`, but on a per-Gateway basis + + The Gateway's GatewayClass may provide its own `parametersRef`. When both are specified, + the merging behavior is implementation specific. + It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the Gateway SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + type: object + listeners: + description: |- + Listeners associated with this Gateway. Listeners define + logical endpoints that are bound on this Gateway's addresses. + At least one Listener MUST be specified. + + ## Distinct Listeners + + Each Listener in a set of Listeners (for example, in a single Gateway) + MUST be _distinct_, in that a traffic flow MUST be able to be assigned to + exactly one listener. (This section uses "set of Listeners" rather than + "Listeners in a single Gateway" because implementations MAY merge configuration + from multiple Gateways onto a single data plane, and these rules _also_ + apply in that case). + + Practically, this means that each listener in a set MUST have a unique + combination of Port, Protocol, and, if supported by the protocol, Hostname. + + Some combinations of port, protocol, and TLS settings are considered + Core support and MUST be supported by implementations based on the objects + they support: + + HTTPRoute + + 1. HTTPRoute, Port: 80, Protocol: HTTP + 2. HTTPRoute, Port: 443, Protocol: HTTPS, TLS Mode: Terminate, TLS keypair provided + + TLSRoute + + 1. TLSRoute, Port: 443, Protocol: TLS, TLS Mode: Passthrough + + "Distinct" Listeners have the following property: + + **The implementation can match inbound requests to a single distinct + Listener**. + + When multiple Listeners share values for fields (for + example, two Listeners with the same Port value), the implementation + can match requests to only one of the Listeners using other + Listener fields. + + When multiple listeners have the same value for the Protocol field, then + each of the Listeners with matching Protocol values MUST have different + values for other fields. + + The set of fields that MUST be different for a Listener differs per protocol. + The following rules define the rules for what fields MUST be considered for + Listeners to be distinct with each protocol currently defined in the + Gateway API spec. + + The set of listeners that all share a protocol value MUST have _different_ + values for _at least one_ of these fields to be distinct: + + * **HTTP, HTTPS, TLS**: Port, Hostname + * **TCP, UDP**: Port + + One **very** important rule to call out involves what happens when an + implementation: + + * Supports TCP protocol Listeners, as well as HTTP, HTTPS, or TLS protocol + Listeners, and + * sees HTTP, HTTPS, or TLS protocols with the same `port` as one with TCP + Protocol. + + In this case all the Listeners that share a port with the + TCP Listener are not distinct and so MUST NOT be accepted. + + If an implementation does not support TCP Protocol Listeners, then the + previous rule does not apply, and the TCP Listeners SHOULD NOT be + accepted. + + Note that the `tls` field is not used for determining if a listener is distinct, because + Listeners that _only_ differ on TLS config will still conflict in all cases. + + ### Listeners that are distinct only by Hostname + + When the Listeners are distinct based only on Hostname, inbound request + hostnames MUST match from the most specific to least specific Hostname + values to choose the correct Listener and its associated set of Routes. + + Exact matches MUST be processed before wildcard matches, and wildcard + matches MUST be processed before fallback (empty Hostname value) + matches. For example, `"foo.example.com"` takes precedence over + `"*.example.com"`, and `"*.example.com"` takes precedence over `""`. + + Additionally, if there are multiple wildcard entries, more specific + wildcard entries must be processed before less specific wildcard entries. + For example, `"*.foo.example.com"` takes precedence over `"*.example.com"`. + + The precise definition here is that the higher the number of dots in the + hostname to the right of the wildcard character, the higher the precedence. + + The wildcard character will match any number of characters _and dots_ to + the left, however, so `"*.example.com"` will match both + `"foo.bar.example.com"` _and_ `"bar.example.com"`. + + ## Handling indistinct Listeners + + If a set of Listeners contains Listeners that are not distinct, then those + Listeners are _Conflicted_, and the implementation MUST set the "Conflicted" + condition in the Listener Status to "True". + + The words "indistinct" and "conflicted" are considered equivalent for the + purpose of this documentation. + + Implementations MAY choose to accept a Gateway with some Conflicted + Listeners only if they only accept the partial Listener set that contains + no Conflicted Listeners. + + Specifically, an implementation MAY accept a partial Listener set subject to + the following rules: + + * The implementation MUST NOT pick one conflicting Listener as the winner. + ALL indistinct Listeners must not be accepted for processing. + * At least one distinct Listener MUST be present, or else the Gateway effectively + contains _no_ Listeners, and must be rejected from processing as a whole. + + The implementation MUST set a "ListenersNotValid" condition on the + Gateway Status when the Gateway contains Conflicted Listeners whether or + not they accept the Gateway. That Condition SHOULD clearly + indicate in the Message which Listeners are conflicted, and which are + Accepted. Additionally, the Listener status for those listeners SHOULD + indicate which Listeners are conflicted and not Accepted. + + ## General Listener behavior + + Note that, for all distinct Listeners, requests SHOULD match at most one Listener. + For example, if Listeners are defined for "foo.example.com" and "*.example.com", a + request to "foo.example.com" SHOULD only be routed using routes attached + to the "foo.example.com" Listener (and not the "*.example.com" Listener). + + If traffic to a Gateway does not match any Listener's hostname (or if + the Listener does not specify a hostname and the request does not match + any attached Route), the request MUST be rejected. The specific mechanism + for rejection depends on the protocol: HTTP returns a 404 status code, + while gRPC returns an Unimplemented status code. + + This concept is known as "Listener Isolation", and it is an Extended feature + of Gateway API. Implementations that do not support Listener Isolation MUST + clearly document this, and MUST NOT claim support for the + `GatewayHTTPListenerIsolation` feature. + + Implementations that _do_ support Listener Isolation SHOULD claim support + for the Extended `GatewayHTTPListenerIsolation` feature and pass the associated + conformance tests. + + ## Compatible Listeners + + A Gateway's Listeners are considered _compatible_ if: + + 1. They are distinct. + 2. The implementation can serve them in compliance with the Addresses + requirement that all Listeners are available on all assigned + addresses. + + Compatible combinations in Extended support are expected to vary across + implementations. A combination that is compatible for one implementation + may not be compatible for another. + + For example, an implementation that cannot serve both TCP and UDP listeners + on the same address, or cannot mix HTTPS and generic TLS listens on the same port + would not consider those cases compatible, even though they are distinct. + + Implementations MAY merge separate Gateways onto a single set of + Addresses if all Listeners across all Gateways are compatible. + + In a future release the MinItems=1 requirement MAY be dropped. + + Support: Core + items: + description: |- + Listener embodies the concept of a logical endpoint where a Gateway accepts + network connections. + properties: + allowedRoutes: + default: + namespaces: + from: Same + description: |- + AllowedRoutes defines the types of routes that MAY be attached to a + Listener and the trusted namespaces where those Route resources MAY be + present. + + Although a client request may match multiple route rules, only one rule + may ultimately receive the request. Matching precedence MUST be + determined in order of the following criteria: + + * The most specific match as defined by the Route type. + * The oldest Route based on creation timestamp. For example, a Route with + a creation timestamp of "2020-09-08 01:02:03" is given precedence over + a Route with a creation timestamp of "2020-09-08 01:02:04". + * If everything else is equivalent, the Route appearing first in + alphabetical order (namespace/name) should be given precedence. For + example, foo/bar is given precedence over foo/baz. + + All valid rules within a Route attached to this Listener should be + implemented. Invalid Route rules can be ignored (sometimes that will mean + the full Route). If a Route rule transitions from valid to invalid, + support for that Route rule should be dropped to ensure consistency. For + example, even if a filter specified by a Route rule is invalid, the rest + of the rules within that Route should still be supported. + + Support: Core + properties: + kinds: + description: |- + Kinds specifies the groups and kinds of Routes that are allowed to bind + to this Gateway Listener. When unspecified or empty, the kinds of Routes + selected are determined using the Listener protocol. + + A RouteGroupKind MUST correspond to kinds of Routes that are compatible + with the application protocol specified in the Listener's Protocol field. + If an implementation does not support or recognize this resource type, it + MUST set the "ResolvedRefs" condition to False for this Listener with the + "InvalidRouteKinds" reason. + + Support: Core + items: + description: RouteGroupKind indicates the group and kind + of a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + namespaces: + default: + from: Same + description: |- + Namespaces indicates namespaces from which Routes may be attached to this + Listener. This is restricted to the namespace of this Gateway by default. + + Support: Core + properties: + from: + default: Same + description: |- + From indicates where Routes will be selected for this Gateway. Possible + values are: + + * All: Routes in all namespaces may be used by this Gateway. + * Selector: Routes in namespaces selected by the selector may be used by + this Gateway. + * Same: Only Routes in the same namespace may be used by this Gateway. + + Support: Core + enum: + - All + - Selector + - Same + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only Routes in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + + Support: Core + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + hostname: + description: |- + Hostname specifies the virtual hostname to match for protocol types that + define this concept. When unspecified, all hostnames are matched. This + field is ignored for protocols that don't require hostname based + matching. + + Implementations MUST apply Hostname matching appropriately for each of + the following protocols: + + * TLS: The Listener Hostname MUST match the SNI. + * HTTP: The Listener Hostname MUST match the Host header of the request. + * HTTPS: The Listener Hostname SHOULD match both the SNI and Host header. + Note that this does not require the SNI and Host header to be the same. + The semantics of this are described in more detail below. + + To ensure security, Section 11.1 of RFC-6066 emphasizes that server + implementations that rely on SNI hostname matching MUST also verify + hostnames within the application protocol. + + Section 9.1.2 of RFC-7540 provides a mechanism for servers to reject the + reuse of a connection by responding with the HTTP 421 Misdirected Request + status code. This indicates that the origin server has rejected the + request because it appears to have been misdirected. + + To detect misdirected requests, Gateways SHOULD match the authority of + the requests with all the SNI hostname(s) configured across all the + Gateway Listeners on the same port and protocol: + + * If another Listener has an exact match or more specific wildcard entry, + the Gateway SHOULD return a 421. + * If the current Listener (selected by SNI matching during ClientHello) + does not match the Host: + * If another Listener does match the Host, the Gateway SHOULD return a + 421. + * If no other Listener matches the Host, the Gateway MUST return a + 404. + + For HTTPRoute and TLSRoute resources, there is an interaction with the + `spec.hostnames` array. When both listener and route specify hostnames, + there MUST be an intersection between the values for a Route to be + accepted. For more information, refer to the Route specific Hostnames + documentation. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + name: + description: |- + Name is the name of the Listener. This name MUST be unique within a + Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + port: + description: |- + Port is the network port. Multiple listeners may use the + same port, subject to the Listener compatibility rules. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: |- + Protocol specifies the network protocol this listener expects to receive. + + Support: Core + maxLength: 255 + minLength: 1 + pattern: ^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9]+$ + type: string + tls: + description: |- + TLS is the TLS configuration for the Listener. This field is required if + the Protocol field is "HTTPS" or "TLS". It is invalid to set this field + if the Protocol field is "HTTP", "TCP", or "UDP". + + The association of SNIs to Certificate defined in ListenerTLSConfig is + defined based on the Hostname field for this listener. + + The GatewayClass MUST use the longest matching SNI out of all + available certificates for any TLS handshake. + + Support: Core + properties: + certificateRefs: + description: |- + CertificateRefs contains a series of references to Kubernetes objects that + contains TLS certificates and private keys. These certificates are used to + establish a TLS handshake for requests that match the hostname of the + associated listener. + + A single CertificateRef to a Kubernetes Secret has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a Listener, but this behavior is implementation-specific. + + References to a resource in different namespace are invalid UNLESS there + is a ReferenceGrant in the target namespace that allows the certificate + to be attached. If a ReferenceGrant does not allow this reference, the + "ResolvedRefs" condition MUST be set to False for this listener with the + "RefNotPermitted" reason. + + This field is required to have at least one element when the mode is set + to "Terminate" (default) and is optional otherwise. + + CertificateRefs can reference to standard Kubernetes resources, i.e. + Secret, or implementation-specific custom resources. + + Support: Core - A single reference to a Kubernetes Secret of type kubernetes.io/tls + + Support: Implementation-specific (More than one reference or other resource types) + items: + description: |- + SecretObjectReference identifies an API object including its namespace, + defaulting to Secret. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + mode: + default: Terminate + description: |- + Mode defines the TLS behavior for the TLS session initiated by the client. + There are two possible modes: + + - Terminate: The TLS session between the downstream client and the + Gateway is terminated at the Gateway. This mode requires certificates + to be specified in some way, such as populating the certificateRefs + field. + - Passthrough: The TLS session is NOT terminated by the Gateway. This + implies that the Gateway can't decipher the TLS stream except for + the ClientHello message of the TLS protocol. The certificateRefs field + is ignored in this mode. + + Support: Core + enum: + - Terminate + - Passthrough + type: string + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + type: object + x-kubernetes-validations: + - message: certificateRefs or options must be specified when + mode is Terminate + rule: 'self.mode == ''Terminate'' ? size(self.certificateRefs) + > 0 || size(self.options) > 0 : true' + required: + - name + - port + - protocol + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: tls must not be specified for protocols ['HTTP', 'TCP', + 'UDP'] + rule: 'self.all(l, l.protocol in [''HTTP'', ''TCP'', ''UDP''] ? + !has(l.tls) : true)' + - message: tls mode must be Terminate for protocol HTTPS + rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode + == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' + - message: hostname must not be specified for protocols ['TCP', 'UDP'] + rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) + || l.hostname == '''') : true)' + - message: Listener name must be unique within the Gateway + rule: self.all(l1, self.exists_one(l2, l1.name == l2.name)) + - message: Combination of port, protocol and hostname must be unique + for each listener + rule: 'self.all(l1, self.exists_one(l2, l1.port == l2.port && l1.protocol + == l2.protocol && (has(l1.hostname) && has(l2.hostname) ? l1.hostname + == l2.hostname : !has(l1.hostname) && !has(l2.hostname))))' + tls: + description: |- + TLS specifies frontend and backend tls configuration for entire gateway. + + Support: Extended + properties: + backend: + description: |- + Backend describes TLS configuration for gateway when connecting + to backends. + + Note that this contains only details for the Gateway as a TLS client, + and does _not_ imply behavior about how to choose which backend should + get a TLS connection. That is determined by the presence of a BackendTLSPolicy. + + Support: Core + properties: + clientCertificateRef: + description: |- + ClientCertificateRef references an object that contains a client certificate + and its associated private key. It can reference standard Kubernetes resources, + i.e., Secret, or implementation-specific custom resources. + + A ClientCertificateRef is considered invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a Secret does not contain the keys + named `tls.crt` and `tls.key`). In this case, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `InvalidClientCertificateRef` + and the Message of the Condition MUST indicate why the reference is invalid. + + * It refers to a resource in another namespace UNLESS there is a ReferenceGrant + in the target namespace that allows the certificate to be attached. + If a ReferenceGrant does not allow this reference, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message MUST be set. + + Support: Core - Reference to a Kubernetes TLS Secret (with the type `kubernetes.io/tls`). + Support: Implementation-specific - Other resource kinds or Secrets with a + different type (e.g., `Opaque`). + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + type: object + frontend: + description: |- + Frontend describes TLS config when client connects to Gateway. + Support: Core + properties: + default: + description: |- + Default specifies the default client certificate validation configuration + for all Listeners handling HTTPS traffic, unless a per-port configuration + is defined. + + support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + perPort: + description: |- + PerPort specifies tls configuration assigned per port. + Per port configuration is optional. Once set this configuration overrides + the default configuration for all Listeners handling HTTPS traffic + that match this port. + Each override port requires a unique TLS configuration. + + support: Core + items: + properties: + port: + description: |- + The Port indicates the Port Number to which the TLS configuration will be + applied. This configuration will be applied to all Listeners handling HTTPS + traffic that match this port. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + tls: + description: |- + TLS store the configuration that will be applied to all Listeners handling + HTTPS traffic and matching given port. + + Support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + For example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + required: + - port + - tls + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - port + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: Port for TLS configuration must be unique within + the Gateway + rule: self.all(t1, self.exists_one(t2, t1.port == t2.port)) + required: + - default + type: object + type: object + required: + - gatewayClassName + - listeners + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: Status defines the current state of Gateway. + properties: + addresses: + description: |- + Addresses lists the network addresses that have been bound to the + Gateway. + + This list may differ from the addresses provided in the spec under some + conditions: + + * no addresses are specified, all addresses are dynamically assigned + * a combination of specified and dynamic addresses are assigned + * a specified address was unusable (e.g. already in use) + items: + description: GatewayStatusAddress describes a network address that + is bound to a Gateway. + oneOf: + - properties: + type: + enum: + - IPAddress + value: + anyOf: + - format: ipv4 + - format: ipv6 + - properties: + type: + not: + enum: + - IPAddress + properties: + type: + default: IPAddress + description: Type of the address. + maxLength: 253 + minLength: 1 + pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + value: + description: |- + Value of the address. The validity of the values will depend + on the type and support by the controller. + + Examples: `1.2.3.4`, `128::1`, `my-ip-address`. + maxLength: 253 + minLength: 1 + type: string + required: + - value + type: object + x-kubernetes-validations: + - message: Hostname value must only contain valid characters (matching + ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"""): + true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + attachedListenerSets: + description: |- + AttachedListenerSets represents the total number of ListenerSets that have been + successfully attached to this Gateway. + + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + + Uses for this field include troubleshooting AttachedListenerSets attachment and + measuring blast radius/impact of changes to a Gateway. + format: int32 + type: integer + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: |- + Conditions describe the current conditions of the Gateway. + + Implementations should prefer to express Gateway conditions + using the `GatewayConditionType` and `GatewayConditionReason` + constants so that operators and tools can converge on a common + vocabulary to describe Gateway state. + + Known condition types are: + + * "Accepted" + * "Programmed" + * "Ready" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + listeners: + description: Listeners provide status for each unique listener port + defined in the Spec. + items: + description: ListenerStatus is the status associated with a Listener. + properties: + attachedRoutes: + description: |- + AttachedRoutes represents the total number of Routes that have been + successfully attached to this Listener. + + Successful attachment of a Route to a Listener is based solely on the + combination of the AllowedRoutes field on the corresponding Listener + and the Route's ParentRefs field. A Route is successfully attached to + a Listener when it is selected by the Listener's AllowedRoutes field + AND the Route has a valid ParentRef selecting the whole Gateway + resource or a specific Listener as a parent resource (more detail on + attachment semantics can be found in the documentation on the various + Route kinds ParentRefs fields). Listener or Route status does not impact + successful attachment, i.e. the AttachedRoutes field count MUST be set + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. + + Uses for this field include troubleshooting Route attachment and + measuring blast radius/impact of changes to a Listener. + format: int32 + type: integer + conditions: + description: Conditions describe the current condition of this + listener. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: Name is the name of the Listener that this status + corresponds to. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + supportedKinds: + description: |- + SupportedKinds is the list indicating the Kinds supported by this + listener. This MUST represent the kinds supported by an implementation for + that Listener configuration. + + If kinds are specified in Spec that are not supported, they MUST NOT + appear in this list and an implementation MUST set the "ResolvedRefs" + condition to "False" with the "InvalidRouteKinds" reason. If both valid + and invalid Route kinds are specified, the implementation MUST + reference the valid Route kinds that have been specified. + items: + description: RouteGroupKind indicates the group and kind of + a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + required: + - attachedRoutes + - conditions + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.gatewayClassName + name: Class + type: string + - jsonPath: .status.addresses[*].value + name: Address + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + Gateway represents an instance of a service-traffic handling infrastructure + by binding Listeners to a set of IP addresses. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of Gateway. + properties: + addresses: + description: |- + Addresses requested for this Gateway. This is optional and behavior can + depend on the implementation. If a value is set in the spec and the + requested address is invalid or unavailable, the implementation MUST + indicate this in an associated entry in GatewayStatus.Conditions. + + The Addresses field represents a request for the address(es) on the + "outside of the Gateway", that traffic bound for this Gateway will use. + This could be the IP address or hostname of an external load balancer or + other networking infrastructure, or some other address that traffic will + be sent to. + + If no Addresses are specified, the implementation MAY schedule the + Gateway in an implementation-specific manner, assigning an appropriate + set of Addresses. + + The implementation MUST bind all Listeners to every GatewayAddress that + it assigns to the Gateway and add a corresponding entry in + GatewayStatus.Addresses. + + Support: Extended + items: + description: GatewaySpecAddress describes an address that can be + bound to a Gateway. + oneOf: + - properties: + type: + enum: + - IPAddress + value: + anyOf: + - format: ipv4 + - format: ipv6 + - properties: + type: + not: + enum: + - IPAddress + properties: + type: + default: IPAddress + description: Type of the address. + maxLength: 253 + minLength: 1 + pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + value: + description: |- + When a value is unspecified, an implementation SHOULD automatically + assign an address matching the requested type if possible. + + If an implementation does not support an empty value, they MUST set the + "Programmed" condition in status to False with a reason of "AddressNotAssigned". + + Examples: `1.2.3.4`, `128::1`, `my-ip-address`. + maxLength: 253 + type: string + type: object + x-kubernetes-validations: + - message: Hostname value must be empty or contain only valid characters + (matching ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? (!has(self.value) || self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$""")): + true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: IPAddress values must be unique + rule: 'self.all(a1, a1.type == ''IPAddress'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + - message: Hostname values must be unique + rule: 'self.all(a1, a1.type == ''Hostname'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + allowedListeners: + description: |- + AllowedListeners defines which ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. + properties: + namespaces: + default: + from: None + description: |- + Namespaces defines which namespaces ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. + properties: + from: + default: None + description: |- + From indicates where ListenerSets can attach to this Gateway. Possible + values are: + + * Same: Only ListenerSets in the same namespace may be attached to this Gateway. + * Selector: ListenerSets in namespaces selected by the selector may be attached to this Gateway. + * All: ListenerSets in all namespaces may be attached to this Gateway. + * None: Only listeners defined in the Gateway's spec are allowed + + The default value None + enum: + - All + - Selector + - Same + - None + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only ListenerSets in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + gatewayClassName: + description: |- + GatewayClassName used for this Gateway. This is the name of a + GatewayClass resource. + maxLength: 253 + minLength: 1 + type: string + infrastructure: + description: |- + Infrastructure defines infrastructure level attributes about this Gateway instance. + + Support: Extended + properties: + annotations: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Annotations that SHOULD be applied to any resources created in response to this Gateway. + + For implementations creating other Kubernetes objects, this should be the `metadata.annotations` field on resources. + For other implementations, this refers to any relevant (implementation specific) "annotations" concepts. + + An implementation may chose to add additional implementation-specific annotations as they see fit. + + Support: Extended + maxProperties: 16 + type: object + x-kubernetes-validations: + - message: Annotation keys must be in the form of an optional + DNS subdomain prefix followed by a required name segment of + up to 63 characters. + rule: self.all(key, key.matches(r"""^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]$""")) + - message: If specified, the annotation key's prefix must be a + DNS subdomain not longer than 253 characters in total. + rule: self.all(key, key.split("/")[0].size() < 253) + labels: + additionalProperties: + description: |- + LabelValue is the value of a label in the Gateway API. This is used for validation + of maps such as Gateway infrastructure labels. This matches the Kubernetes + label validation rules: + * must be 63 characters or less (can be empty), + * unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]), + * could contain dashes (-), underscores (_), dots (.), and alphanumerics between. + + Valid values include: + + * MyValue + * my.name + * 123-my-value + maxLength: 63 + minLength: 0 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + description: |- + Labels that SHOULD be applied to any resources created in response to this Gateway. + + For implementations creating other Kubernetes objects, this should be the `metadata.labels` field on resources. + For other implementations, this refers to any relevant (implementation specific) "labels" concepts. + + An implementation may chose to add additional implementation-specific labels as they see fit. + + If an implementation maps these labels to Pods, or any other resource that would need to be recreated when labels + change, it SHOULD clearly warn about this behavior in documentation. + + Support: Extended + maxProperties: 8 + type: object + x-kubernetes-validations: + - message: Label keys must be in the form of an optional DNS subdomain + prefix followed by a required name segment of up to 63 characters. + rule: self.all(key, key.matches(r"""^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]$""")) + - message: If specified, the label key's prefix must be a DNS + subdomain not longer than 253 characters in total. + rule: self.all(key, key.split("/")[0].size() < 253) + parametersRef: + description: |- + ParametersRef is a reference to a resource that contains the configuration + parameters corresponding to the Gateway. This is optional if the + controller does not require any additional configuration. + + This follows the same semantics as GatewayClass's `parametersRef`, but on a per-Gateway basis + + The Gateway's GatewayClass may provide its own `parametersRef`. When both are specified, + the merging behavior is implementation specific. + It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the Gateway SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + type: object + listeners: + description: |- + Listeners associated with this Gateway. Listeners define + logical endpoints that are bound on this Gateway's addresses. + At least one Listener MUST be specified. + + ## Distinct Listeners + + Each Listener in a set of Listeners (for example, in a single Gateway) + MUST be _distinct_, in that a traffic flow MUST be able to be assigned to + exactly one listener. (This section uses "set of Listeners" rather than + "Listeners in a single Gateway" because implementations MAY merge configuration + from multiple Gateways onto a single data plane, and these rules _also_ + apply in that case). + + Practically, this means that each listener in a set MUST have a unique + combination of Port, Protocol, and, if supported by the protocol, Hostname. + + Some combinations of port, protocol, and TLS settings are considered + Core support and MUST be supported by implementations based on the objects + they support: + + HTTPRoute + + 1. HTTPRoute, Port: 80, Protocol: HTTP + 2. HTTPRoute, Port: 443, Protocol: HTTPS, TLS Mode: Terminate, TLS keypair provided + + TLSRoute + + 1. TLSRoute, Port: 443, Protocol: TLS, TLS Mode: Passthrough + + "Distinct" Listeners have the following property: + + **The implementation can match inbound requests to a single distinct + Listener**. + + When multiple Listeners share values for fields (for + example, two Listeners with the same Port value), the implementation + can match requests to only one of the Listeners using other + Listener fields. + + When multiple listeners have the same value for the Protocol field, then + each of the Listeners with matching Protocol values MUST have different + values for other fields. + + The set of fields that MUST be different for a Listener differs per protocol. + The following rules define the rules for what fields MUST be considered for + Listeners to be distinct with each protocol currently defined in the + Gateway API spec. + + The set of listeners that all share a protocol value MUST have _different_ + values for _at least one_ of these fields to be distinct: + + * **HTTP, HTTPS, TLS**: Port, Hostname + * **TCP, UDP**: Port + + One **very** important rule to call out involves what happens when an + implementation: + + * Supports TCP protocol Listeners, as well as HTTP, HTTPS, or TLS protocol + Listeners, and + * sees HTTP, HTTPS, or TLS protocols with the same `port` as one with TCP + Protocol. + + In this case all the Listeners that share a port with the + TCP Listener are not distinct and so MUST NOT be accepted. + + If an implementation does not support TCP Protocol Listeners, then the + previous rule does not apply, and the TCP Listeners SHOULD NOT be + accepted. + + Note that the `tls` field is not used for determining if a listener is distinct, because + Listeners that _only_ differ on TLS config will still conflict in all cases. + + ### Listeners that are distinct only by Hostname + + When the Listeners are distinct based only on Hostname, inbound request + hostnames MUST match from the most specific to least specific Hostname + values to choose the correct Listener and its associated set of Routes. + + Exact matches MUST be processed before wildcard matches, and wildcard + matches MUST be processed before fallback (empty Hostname value) + matches. For example, `"foo.example.com"` takes precedence over + `"*.example.com"`, and `"*.example.com"` takes precedence over `""`. + + Additionally, if there are multiple wildcard entries, more specific + wildcard entries must be processed before less specific wildcard entries. + For example, `"*.foo.example.com"` takes precedence over `"*.example.com"`. + + The precise definition here is that the higher the number of dots in the + hostname to the right of the wildcard character, the higher the precedence. + + The wildcard character will match any number of characters _and dots_ to + the left, however, so `"*.example.com"` will match both + `"foo.bar.example.com"` _and_ `"bar.example.com"`. + + ## Handling indistinct Listeners + + If a set of Listeners contains Listeners that are not distinct, then those + Listeners are _Conflicted_, and the implementation MUST set the "Conflicted" + condition in the Listener Status to "True". + + The words "indistinct" and "conflicted" are considered equivalent for the + purpose of this documentation. + + Implementations MAY choose to accept a Gateway with some Conflicted + Listeners only if they only accept the partial Listener set that contains + no Conflicted Listeners. + + Specifically, an implementation MAY accept a partial Listener set subject to + the following rules: + + * The implementation MUST NOT pick one conflicting Listener as the winner. + ALL indistinct Listeners must not be accepted for processing. + * At least one distinct Listener MUST be present, or else the Gateway effectively + contains _no_ Listeners, and must be rejected from processing as a whole. + + The implementation MUST set a "ListenersNotValid" condition on the + Gateway Status when the Gateway contains Conflicted Listeners whether or + not they accept the Gateway. That Condition SHOULD clearly + indicate in the Message which Listeners are conflicted, and which are + Accepted. Additionally, the Listener status for those listeners SHOULD + indicate which Listeners are conflicted and not Accepted. + + ## General Listener behavior + + Note that, for all distinct Listeners, requests SHOULD match at most one Listener. + For example, if Listeners are defined for "foo.example.com" and "*.example.com", a + request to "foo.example.com" SHOULD only be routed using routes attached + to the "foo.example.com" Listener (and not the "*.example.com" Listener). + + If traffic to a Gateway does not match any Listener's hostname (or if + the Listener does not specify a hostname and the request does not match + any attached Route), the request MUST be rejected. The specific mechanism + for rejection depends on the protocol: HTTP returns a 404 status code, + while gRPC returns an Unimplemented status code. + + This concept is known as "Listener Isolation", and it is an Extended feature + of Gateway API. Implementations that do not support Listener Isolation MUST + clearly document this, and MUST NOT claim support for the + `GatewayHTTPListenerIsolation` feature. + + Implementations that _do_ support Listener Isolation SHOULD claim support + for the Extended `GatewayHTTPListenerIsolation` feature and pass the associated + conformance tests. + + ## Compatible Listeners + + A Gateway's Listeners are considered _compatible_ if: + + 1. They are distinct. + 2. The implementation can serve them in compliance with the Addresses + requirement that all Listeners are available on all assigned + addresses. + + Compatible combinations in Extended support are expected to vary across + implementations. A combination that is compatible for one implementation + may not be compatible for another. + + For example, an implementation that cannot serve both TCP and UDP listeners + on the same address, or cannot mix HTTPS and generic TLS listens on the same port + would not consider those cases compatible, even though they are distinct. + + Implementations MAY merge separate Gateways onto a single set of + Addresses if all Listeners across all Gateways are compatible. + + In a future release the MinItems=1 requirement MAY be dropped. + + Support: Core + items: + description: |- + Listener embodies the concept of a logical endpoint where a Gateway accepts + network connections. + properties: + allowedRoutes: + default: + namespaces: + from: Same + description: |- + AllowedRoutes defines the types of routes that MAY be attached to a + Listener and the trusted namespaces where those Route resources MAY be + present. + + Although a client request may match multiple route rules, only one rule + may ultimately receive the request. Matching precedence MUST be + determined in order of the following criteria: + + * The most specific match as defined by the Route type. + * The oldest Route based on creation timestamp. For example, a Route with + a creation timestamp of "2020-09-08 01:02:03" is given precedence over + a Route with a creation timestamp of "2020-09-08 01:02:04". + * If everything else is equivalent, the Route appearing first in + alphabetical order (namespace/name) should be given precedence. For + example, foo/bar is given precedence over foo/baz. + + All valid rules within a Route attached to this Listener should be + implemented. Invalid Route rules can be ignored (sometimes that will mean + the full Route). If a Route rule transitions from valid to invalid, + support for that Route rule should be dropped to ensure consistency. For + example, even if a filter specified by a Route rule is invalid, the rest + of the rules within that Route should still be supported. + + Support: Core + properties: + kinds: + description: |- + Kinds specifies the groups and kinds of Routes that are allowed to bind + to this Gateway Listener. When unspecified or empty, the kinds of Routes + selected are determined using the Listener protocol. + + A RouteGroupKind MUST correspond to kinds of Routes that are compatible + with the application protocol specified in the Listener's Protocol field. + If an implementation does not support or recognize this resource type, it + MUST set the "ResolvedRefs" condition to False for this Listener with the + "InvalidRouteKinds" reason. + + Support: Core + items: + description: RouteGroupKind indicates the group and kind + of a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + namespaces: + default: + from: Same + description: |- + Namespaces indicates namespaces from which Routes may be attached to this + Listener. This is restricted to the namespace of this Gateway by default. + + Support: Core + properties: + from: + default: Same + description: |- + From indicates where Routes will be selected for this Gateway. Possible + values are: + + * All: Routes in all namespaces may be used by this Gateway. + * Selector: Routes in namespaces selected by the selector may be used by + this Gateway. + * Same: Only Routes in the same namespace may be used by this Gateway. + + Support: Core + enum: + - All + - Selector + - Same + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only Routes in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + + Support: Core + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + hostname: + description: |- + Hostname specifies the virtual hostname to match for protocol types that + define this concept. When unspecified, all hostnames are matched. This + field is ignored for protocols that don't require hostname based + matching. + + Implementations MUST apply Hostname matching appropriately for each of + the following protocols: + + * TLS: The Listener Hostname MUST match the SNI. + * HTTP: The Listener Hostname MUST match the Host header of the request. + * HTTPS: The Listener Hostname SHOULD match both the SNI and Host header. + Note that this does not require the SNI and Host header to be the same. + The semantics of this are described in more detail below. + + To ensure security, Section 11.1 of RFC-6066 emphasizes that server + implementations that rely on SNI hostname matching MUST also verify + hostnames within the application protocol. + + Section 9.1.2 of RFC-7540 provides a mechanism for servers to reject the + reuse of a connection by responding with the HTTP 421 Misdirected Request + status code. This indicates that the origin server has rejected the + request because it appears to have been misdirected. + + To detect misdirected requests, Gateways SHOULD match the authority of + the requests with all the SNI hostname(s) configured across all the + Gateway Listeners on the same port and protocol: + + * If another Listener has an exact match or more specific wildcard entry, + the Gateway SHOULD return a 421. + * If the current Listener (selected by SNI matching during ClientHello) + does not match the Host: + * If another Listener does match the Host, the Gateway SHOULD return a + 421. + * If no other Listener matches the Host, the Gateway MUST return a + 404. + + For HTTPRoute and TLSRoute resources, there is an interaction with the + `spec.hostnames` array. When both listener and route specify hostnames, + there MUST be an intersection between the values for a Route to be + accepted. For more information, refer to the Route specific Hostnames + documentation. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + name: + description: |- + Name is the name of the Listener. This name MUST be unique within a + Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + port: + description: |- + Port is the network port. Multiple listeners may use the + same port, subject to the Listener compatibility rules. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: |- + Protocol specifies the network protocol this listener expects to receive. + + Support: Core + maxLength: 255 + minLength: 1 + pattern: ^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9]+$ + type: string + tls: + description: |- + TLS is the TLS configuration for the Listener. This field is required if + the Protocol field is "HTTPS" or "TLS". It is invalid to set this field + if the Protocol field is "HTTP", "TCP", or "UDP". + + The association of SNIs to Certificate defined in ListenerTLSConfig is + defined based on the Hostname field for this listener. + + The GatewayClass MUST use the longest matching SNI out of all + available certificates for any TLS handshake. + + Support: Core + properties: + certificateRefs: + description: |- + CertificateRefs contains a series of references to Kubernetes objects that + contains TLS certificates and private keys. These certificates are used to + establish a TLS handshake for requests that match the hostname of the + associated listener. + + A single CertificateRef to a Kubernetes Secret has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a Listener, but this behavior is implementation-specific. + + References to a resource in different namespace are invalid UNLESS there + is a ReferenceGrant in the target namespace that allows the certificate + to be attached. If a ReferenceGrant does not allow this reference, the + "ResolvedRefs" condition MUST be set to False for this listener with the + "RefNotPermitted" reason. + + This field is required to have at least one element when the mode is set + to "Terminate" (default) and is optional otherwise. + + CertificateRefs can reference to standard Kubernetes resources, i.e. + Secret, or implementation-specific custom resources. + + Support: Core - A single reference to a Kubernetes Secret of type kubernetes.io/tls + + Support: Implementation-specific (More than one reference or other resource types) + items: + description: |- + SecretObjectReference identifies an API object including its namespace, + defaulting to Secret. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + mode: + default: Terminate + description: |- + Mode defines the TLS behavior for the TLS session initiated by the client. + There are two possible modes: + + - Terminate: The TLS session between the downstream client and the + Gateway is terminated at the Gateway. This mode requires certificates + to be specified in some way, such as populating the certificateRefs + field. + - Passthrough: The TLS session is NOT terminated by the Gateway. This + implies that the Gateway can't decipher the TLS stream except for + the ClientHello message of the TLS protocol. The certificateRefs field + is ignored in this mode. + + Support: Core + enum: + - Terminate + - Passthrough + type: string + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + type: object + x-kubernetes-validations: + - message: certificateRefs or options must be specified when + mode is Terminate + rule: 'self.mode == ''Terminate'' ? size(self.certificateRefs) + > 0 || size(self.options) > 0 : true' + required: + - name + - port + - protocol + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: tls must not be specified for protocols ['HTTP', 'TCP', + 'UDP'] + rule: 'self.all(l, l.protocol in [''HTTP'', ''TCP'', ''UDP''] ? + !has(l.tls) : true)' + - message: tls mode must be Terminate for protocol HTTPS + rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode + == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' + - message: hostname must not be specified for protocols ['TCP', 'UDP'] + rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) + || l.hostname == '''') : true)' + - message: Listener name must be unique within the Gateway + rule: self.all(l1, self.exists_one(l2, l1.name == l2.name)) + - message: Combination of port, protocol and hostname must be unique + for each listener + rule: 'self.all(l1, self.exists_one(l2, l1.port == l2.port && l1.protocol + == l2.protocol && (has(l1.hostname) && has(l2.hostname) ? l1.hostname + == l2.hostname : !has(l1.hostname) && !has(l2.hostname))))' + tls: + description: |- + TLS specifies frontend and backend tls configuration for entire gateway. + + Support: Extended + properties: + backend: + description: |- + Backend describes TLS configuration for gateway when connecting + to backends. + + Note that this contains only details for the Gateway as a TLS client, + and does _not_ imply behavior about how to choose which backend should + get a TLS connection. That is determined by the presence of a BackendTLSPolicy. + + Support: Core + properties: + clientCertificateRef: + description: |- + ClientCertificateRef references an object that contains a client certificate + and its associated private key. It can reference standard Kubernetes resources, + i.e., Secret, or implementation-specific custom resources. + + A ClientCertificateRef is considered invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a Secret does not contain the keys + named `tls.crt` and `tls.key`). In this case, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `InvalidClientCertificateRef` + and the Message of the Condition MUST indicate why the reference is invalid. + + * It refers to a resource in another namespace UNLESS there is a ReferenceGrant + in the target namespace that allows the certificate to be attached. + If a ReferenceGrant does not allow this reference, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message MUST be set. + + Support: Core - Reference to a Kubernetes TLS Secret (with the type `kubernetes.io/tls`). + Support: Implementation-specific - Other resource kinds or Secrets with a + different type (e.g., `Opaque`). + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + type: object + frontend: + description: |- + Frontend describes TLS config when client connects to Gateway. + Support: Core + properties: + default: + description: |- + Default specifies the default client certificate validation configuration + for all Listeners handling HTTPS traffic, unless a per-port configuration + is defined. + + support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + perPort: + description: |- + PerPort specifies tls configuration assigned per port. + Per port configuration is optional. Once set this configuration overrides + the default configuration for all Listeners handling HTTPS traffic + that match this port. + Each override port requires a unique TLS configuration. + + support: Core + items: + properties: + port: + description: |- + The Port indicates the Port Number to which the TLS configuration will be + applied. This configuration will be applied to all Listeners handling HTTPS + traffic that match this port. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + tls: + description: |- + TLS store the configuration that will be applied to all Listeners handling + HTTPS traffic and matching given port. + + Support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + For example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + required: + - port + - tls + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - port + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: Port for TLS configuration must be unique within + the Gateway + rule: self.all(t1, self.exists_one(t2, t1.port == t2.port)) + required: + - default + type: object + type: object + required: + - gatewayClassName + - listeners + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: Status defines the current state of Gateway. + properties: + addresses: + description: |- + Addresses lists the network addresses that have been bound to the + Gateway. + + This list may differ from the addresses provided in the spec under some + conditions: + + * no addresses are specified, all addresses are dynamically assigned + * a combination of specified and dynamic addresses are assigned + * a specified address was unusable (e.g. already in use) + items: + description: GatewayStatusAddress describes a network address that + is bound to a Gateway. + oneOf: + - properties: + type: + enum: + - IPAddress + value: + anyOf: + - format: ipv4 + - format: ipv6 + - properties: + type: + not: + enum: + - IPAddress + properties: + type: + default: IPAddress + description: Type of the address. + maxLength: 253 + minLength: 1 + pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + value: + description: |- + Value of the address. The validity of the values will depend + on the type and support by the controller. + + Examples: `1.2.3.4`, `128::1`, `my-ip-address`. + maxLength: 253 + minLength: 1 + type: string + required: + - value + type: object + x-kubernetes-validations: + - message: Hostname value must only contain valid characters (matching + ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"""): + true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + attachedListenerSets: + description: |- + AttachedListenerSets represents the total number of ListenerSets that have been + successfully attached to this Gateway. + + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + + Uses for this field include troubleshooting AttachedListenerSets attachment and + measuring blast radius/impact of changes to a Gateway. + format: int32 + type: integer + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: |- + Conditions describe the current conditions of the Gateway. + + Implementations should prefer to express Gateway conditions + using the `GatewayConditionType` and `GatewayConditionReason` + constants so that operators and tools can converge on a common + vocabulary to describe Gateway state. + + Known condition types are: + + * "Accepted" + * "Programmed" + * "Ready" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + listeners: + description: Listeners provide status for each unique listener port + defined in the Spec. + items: + description: ListenerStatus is the status associated with a Listener. + properties: + attachedRoutes: + description: |- + AttachedRoutes represents the total number of Routes that have been + successfully attached to this Listener. + + Successful attachment of a Route to a Listener is based solely on the + combination of the AllowedRoutes field on the corresponding Listener + and the Route's ParentRefs field. A Route is successfully attached to + a Listener when it is selected by the Listener's AllowedRoutes field + AND the Route has a valid ParentRef selecting the whole Gateway + resource or a specific Listener as a parent resource (more detail on + attachment semantics can be found in the documentation on the various + Route kinds ParentRefs fields). Listener or Route status does not impact + successful attachment, i.e. the AttachedRoutes field count MUST be set + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. + + Uses for this field include troubleshooting Route attachment and + measuring blast radius/impact of changes to a Listener. + format: int32 + type: integer + conditions: + description: Conditions describe the current condition of this + listener. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: Name is the name of the Listener that this status + corresponds to. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + supportedKinds: + description: |- + SupportedKinds is the list indicating the Kinds supported by this + listener. This MUST represent the kinds supported by an implementation for + that Listener configuration. + + If kinds are specified in Spec that are not supported, they MUST NOT + appear in this list and an implementation MUST set the "ResolvedRefs" + condition to "False" with the "InvalidRouteKinds" reason. If both valid + and invalid Route kinds are specified, the implementation MUST + reference the valid Route kinds that have been specified. + items: + description: RouteGroupKind indicates the group and kind of + a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + required: + - attachedRoutes + - conditions + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_grpcroutes.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: grpcroutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: GRPCRoute + listKind: GRPCRouteList + plural: grpcroutes + singular: grpcroute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + GRPCRoute provides a way to route gRPC requests. This includes the capability + to match requests by hostname, gRPC service, gRPC method, or HTTP/2 header. + Filters can be used to specify additional processing steps. Backends specify + where matching requests will be routed. + + GRPCRoute falls under extended support within the Gateway API. Within the + following specification, the word "MUST" indicates that an implementation + supporting GRPCRoute must conform to the indicated requirement, but an + implementation not supporting this route type need not follow the requirement + unless explicitly indicated. + + Implementations supporting `GRPCRoute` with the `HTTPS` `ProtocolType` MUST + accept HTTP/2 connections without an initial upgrade from HTTP/1.1, i.e. via + ALPN. If the implementation does not support this, then it MUST set the + "Accepted" condition to "False" for the affected listener with a reason of + "UnsupportedProtocol". Implementations MAY also accept HTTP/2 connections + with an upgrade from HTTP/1. + + Implementations supporting `GRPCRoute` with the `HTTP` `ProtocolType` MUST + support HTTP/2 over cleartext TCP (h2c, + https://www.rfc-editor.org/rfc/rfc7540#section-3.1) without an initial + upgrade from HTTP/1.1, i.e. with prior knowledge + (https://www.rfc-editor.org/rfc/rfc7540#section-3.4). If the implementation + does not support this, then it MUST set the "Accepted" condition to "False" + for the affected listener with a reason of "UnsupportedProtocol". + Implementations MAY also accept HTTP/2 connections with an upgrade from + HTTP/1, i.e. without prior knowledge. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of GRPCRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of hostnames to match against the GRPC + Host header to select a GRPCRoute to process the request. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label MUST appear by itself as the first label. + + If a hostname is specified by both the Listener and GRPCRoute, there + MUST be at least one intersecting hostname for the GRPCRoute to be + attached to the Listener. For example: + + * A Listener with `test.example.com` as the hostname matches GRPCRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches GRPCRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `test.example.com` and `*.example.com` would both match. On the other + hand, `example.com` and `test.example.net` would not match. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + If both the Listener and GRPCRoute have specified hostnames, any + GRPCRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + GRPCRoute specified `test.example.com` and `test.example.net`, + `test.example.net` MUST NOT be considered for a match. + + If both the Listener and GRPCRoute have specified hostnames, and none + match with the criteria above, then the GRPCRoute MUST NOT be accepted by + the implementation. The implementation MUST raise an 'Accepted' Condition + with a status of `False` in the corresponding RouteParentStatus. + + If a Route (A) of type HTTPRoute or GRPCRoute is attached to a + Listener and that listener already has another Route (B) of the other + type attached and the intersection of the hostnames of A and B is + non-empty, then the implementation MUST accept exactly one of these two + routes, determined by the following criteria, in order: + + * The oldest Route based on creation timestamp. + * The Route appearing first in alphabetical order by + "{namespace}/{name}". + + The rejected Route MUST raise an 'Accepted' condition with a status of + 'False' in the corresponding RouteParentStatus. + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of GRPC matchers, filters and actions. + items: + description: |- + GRPCRouteRule defines the semantics for matching a gRPC request based on + conditions (matches), processing it (filters), and forwarding the request to + an API object (backendRefs). + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. + + Failure behavior here depends on how many BackendRefs are specified and + how many are invalid. + + If *all* entries in BackendRefs are invalid, and there are also no filters + specified in this route rule, *all* traffic which matches this rule MUST + receive an `UNAVAILABLE` status. + + See the GRPCBackendRef definition for the rules about what makes a single + GRPCBackendRef invalid. + + When a GRPCBackendRef is invalid, `UNAVAILABLE` statuses MUST be returned for + requests that would have otherwise been routed to an invalid backend. If + multiple backends are specified, and some are invalid, the proportion of + requests that would otherwise have been routed to an invalid backend + MUST receive an `UNAVAILABLE` status. + + For example, if two backends are specified with equal weights, and one is + invalid, 50 percent of traffic MUST receive an `UNAVAILABLE` status. + Implementations may choose how that 50 percent is determined. + + Support: Core for Kubernetes Service + + Support: Implementation-specific for any other resource + + Support for weight: Core + items: + description: |- + GRPCBackendRef defines how a GRPCRoute forwards a gRPC request. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + properties: + filters: + description: |- + Filters defined at this level MUST be executed if and only if the + request is being forwarded to the backend defined here. + + Support: Implementation-specific (For broader support of filters, use the + Filters field in GRPCRouteRule.) + items: + description: |- + GRPCRouteFilter defines processing steps that must be completed during the + request or response lifecycle. GRPCRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + Support: Implementation-specific + + This filter can be used multiple times within the same rule. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal + to denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be + specified in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations supporting GRPCRoute MUST support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` MUST be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + enum: + - ResponseHeaderModifier + - RequestHeaderModifier + - RequestMirror + - ExtensionRef + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: filter.requestHeaderModifier must be nil + if the filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil + if the filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for + RequestMirror filter.type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for + ExtensionRef filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + filters: + description: |- + Filters define the filters that are applied to requests that match + this rule. + + The effects of ordering of multiple behaviors are currently unspecified. + This can change in the future based on feedback during the alpha stage. + + Conformance-levels at this level are defined based on the type of filter: + + - ALL core filters MUST be supported by all implementations that support + GRPCRoute. + - Implementers are encouraged to support extended filters. + - Implementation-specific custom filters have no API guarantees across + implementations. + + Specifying the same filter multiple times is not supported unless explicitly + indicated in the filter. + + If an implementation cannot support a combination of filters, it must clearly + document that limitation. In cases where incompatible or unsupported + filters are specified and cause the `Accepted` condition to be set to status + `False`, implementations may use the `IncompatibleFilters` reason to specify + this configuration error. + + Support: Core + items: + description: |- + GRPCRouteFilter defines processing steps that must be completed during the + request or response lifecycle. GRPCRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + Support: Implementation-specific + + This filter can be used multiple times within the same rule. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to + denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be specified + in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations supporting GRPCRoute MUST support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` MUST be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + enum: + - ResponseHeaderModifier + - RequestHeaderModifier + - RequestMirror + - ExtensionRef + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: filter.requestHeaderModifier must be nil if the + filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil if the + filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for RequestMirror + filter.type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for ExtensionRef + filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + matches: + description: |- + Matches define conditions used for matching the rule against incoming + gRPC requests. Each match is independent, i.e. this rule will be matched + if **any** one of the matches is satisfied. + + For example, take the following matches configuration: + + ``` + matches: + - method: + service: foo.bar + headers: + values: + version: 2 + - method: + service: foo.bar.v2 + ``` + + For a request to match against this rule, it MUST satisfy + EITHER of the two conditions: + + - service of foo.bar AND contains the header `version: 2` + - service of foo.bar.v2 + + See the documentation for GRPCRouteMatch on how to specify multiple + match conditions to be ANDed together. + + If no matches are specified, the implementation MUST match every gRPC request. + + Proxy or Load Balancer routing configuration generated from GRPCRoutes + MUST prioritize rules based on the following criteria, continuing on + ties. Merging MUST not be done between GRPCRoutes and HTTPRoutes. + Precedence MUST be given to the rule with the largest number of: + + * Characters in a matching non-wildcard hostname. + * Characters in a matching hostname. + * Characters in a matching service. + * Characters in a matching method. + * Header matches. + + If ties still exist across multiple Routes, matching precedence MUST be + determined in order of the following criteria, continuing on ties: + + * The oldest Route based on creation timestamp. + * The Route appearing first in alphabetical order by + "{namespace}/{name}". + + If ties still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching rule meeting + the above criteria. + items: + description: |- + GRPCRouteMatch defines the predicate used to match requests to a given + action. Multiple match types are ANDed together, i.e. the match will + evaluate to true only if all conditions are satisfied. + + For example, the match below will match a gRPC request only if its service + is `foo` AND it contains the `version: v1` header: + + ``` + matches: + - method: + type: Exact + service: "foo" + - headers: + name: "version" + value "v1" + + ``` + properties: + headers: + description: |- + Headers specifies gRPC request header matchers. Multiple match values are + ANDed together, meaning, a request MUST match all the specified headers + to select the route. + items: + description: |- + GRPCHeaderMatch describes how to select a gRPC route by matching gRPC request + headers. + properties: + name: + description: |- + Name is the name of the gRPC Header to be matched. + + If multiple entries specify equivalent header names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: Type specifies how to match against + the value of the header. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of the gRPC Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: |- + Method specifies a gRPC request service/method matcher. If this field is + not specified, all services and methods will match. + properties: + method: + description: |- + Value of the method to match against. If left empty or omitted, will + match all services. + + At least one of Service and Method MUST be a non-empty string. + maxLength: 1024 + type: string + service: + description: |- + Value of the service to match against. If left empty or omitted, will + match any service. + + At least one of Service and Method MUST be a non-empty string. + maxLength: 1024 + type: string + type: + default: Exact + description: |- + Type specifies how to match against the service and/or method. + Support: Core (Exact with service and method specified) + + Support: Implementation-specific (Exact with method specified but no service specified) + + Support: Implementation-specific (RegularExpression) + enum: + - Exact + - RegularExpression + type: string + type: object + x-kubernetes-validations: + - message: One or both of 'service' or 'method' must be + specified + rule: 'has(self.type) ? has(self.service) || has(self.method) + : true' + - message: service must only contain valid characters + (matching ^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$) + rule: '(!has(self.type) || self.type == ''Exact'') && + has(self.service) ? self.service.matches(r"""^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$"""): + true' + - message: method must only contain valid characters (matching + ^[A-Za-z_][A-Za-z_0-9]*$) + rule: '(!has(self.type) || self.type == ''Exact'') && + has(self.method) ? self.method.matches(r"""^[A-Za-z_][A-Za-z_0-9]*$"""): + true' + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + Name is the name of the route rule. This name MUST be unique within a Route if it is set. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: object + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: While 16 rules and 64 matches per rule are allowed, the + total number of matches across all rules in a route must be less + than 128 + rule: '(self.size() > 0 ? (has(self[0].matches) ? self[0].matches.size() + : 0) : 0) + (self.size() > 1 ? (has(self[1].matches) ? self[1].matches.size() + : 0) : 0) + (self.size() > 2 ? (has(self[2].matches) ? self[2].matches.size() + : 0) : 0) + (self.size() > 3 ? (has(self[3].matches) ? self[3].matches.size() + : 0) : 0) + (self.size() > 4 ? (has(self[4].matches) ? self[4].matches.size() + : 0) : 0) + (self.size() > 5 ? (has(self[5].matches) ? self[5].matches.size() + : 0) : 0) + (self.size() > 6 ? (has(self[6].matches) ? self[6].matches.size() + : 0) : 0) + (self.size() > 7 ? (has(self[7].matches) ? self[7].matches.size() + : 0) : 0) + (self.size() > 8 ? (has(self[8].matches) ? self[8].matches.size() + : 0) : 0) + (self.size() > 9 ? (has(self[9].matches) ? self[9].matches.size() + : 0) : 0) + (self.size() > 10 ? (has(self[10].matches) ? self[10].matches.size() + : 0) : 0) + (self.size() > 11 ? (has(self[11].matches) ? self[11].matches.size() + : 0) : 0) + (self.size() > 12 ? (has(self[12].matches) ? self[12].matches.size() + : 0) : 0) + (self.size() > 13 ? (has(self[13].matches) ? self[13].matches.size() + : 0) : 0) + (self.size() > 14 ? (has(self[14].matches) ? self[14].matches.size() + : 0) : 0) + (self.size() > 15 ? (has(self[15].matches) ? self[15].matches.size() + : 0) : 0) <= 128' + type: object + status: + description: Status defines the current state of GRPCRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_httproutes.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: httproutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: HTTPRoute + listKind: HTTPRouteList + plural: httproutes + singular: httproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + HTTPRoute provides a way to route HTTP requests. This includes the capability + to match requests by hostname, path, header, or query param. Filters can be + used to specify additional processing steps. Backends specify where matching + requests should be routed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of hostnames that should match against the HTTP Host + header to select a HTTPRoute used to process the request. Implementations + MUST ignore any port value specified in the HTTP Host header while + performing a match and (absent of any applicable header modification + configuration) MUST forward this header unmodified to the backend. + + Valid values for Hostnames are determined by RFC 1123 definition of a + hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + If a hostname is specified by both the Listener and HTTPRoute, there + must be at least one intersecting hostname for the HTTPRoute to be + attached to the Listener. For example: + + * A Listener with `test.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `*.example.com`, `test.example.com`, and `foo.test.example.com` would + all match. On the other hand, `example.com` and `test.example.net` would + not match. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + If both the Listener and HTTPRoute have specified hostnames, any + HTTPRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + HTTPRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. + + If both the Listener and HTTPRoute have specified hostnames, and none + match with the criteria above, then the HTTPRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status of + `False` in the corresponding RouteParentStatus. + + In the event that multiple HTTPRoutes specify intersecting hostnames (e.g. + overlapping wildcard matching and exact matching hostnames), precedence must + be given to rules from the HTTPRoute with the largest number of: + + * Characters in a matching non-wildcard hostname. + * Characters in a matching hostname. + + If ties exist across multiple Routes, the matching precedence rules for + HTTPRouteMatches takes over. + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: |- + HTTPRouteRule defines semantics for matching an HTTP request based on + conditions (matches), processing it (filters), and forwarding the request to + an API object (backendRefs). + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. + + Failure behavior here depends on how many BackendRefs are specified and + how many are invalid. + + If *all* entries in BackendRefs are invalid, and there are also no filters + specified in this route rule, *all* traffic which matches this rule MUST + receive a 500 status code. + + See the HTTPBackendRef definition for the rules about what makes a single + HTTPBackendRef invalid. + + When a HTTPBackendRef is invalid, 500 status codes MUST be returned for + requests that would have otherwise been routed to an invalid backend. If + multiple backends are specified, and some are invalid, the proportion of + requests that would otherwise have been routed to an invalid backend + MUST receive a 500 status code. + + For example, if two backends are specified with equal weights, and one is + invalid, 50 percent of traffic must receive a 500. Implementations may + choose how that 50 percent is determined. + + When a HTTPBackendRef refers to a Service that has no ready endpoints, + implementations SHOULD return a 503 for requests to that backend instead. + If an implementation chooses to do this, all of the above rules for 500 responses + MUST also apply for responses that return a 503. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Core + items: + description: |- + HTTPBackendRef defines how a HTTPRoute forwards a HTTP request. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + properties: + filters: + description: |- + Filters defined at this level should be executed if and only if the + request is being forwarded to the backend defined here. + + Support: Implementation-specific (For broader support of filters, use the + Filters field in HTTPRouteRule.) + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `allowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `allowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + + If the configuration contains the wildcard `*` in `allowHeaders` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Headers` + response header may either contain the wildcard `*` or echo the value + of the `Access-Control-Request-Headers` request header. + + If the configuration contains the wildcard `*` in `allowHeaders` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Headers` response header. Instead, it must + return one or more header names matching the value of the + `Access-Control-Request-Headers` request header. + If the `Access-Control-Request-Headers` header is not present in the + request, the gateway must omit the `Access-Control-Allow-Headers` + response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `allowMethods` field. + + When the `allowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `allowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If the configuration contains the wildcard `*` in `allowMethods` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Methods` + response header may either contain the wildcard `*` or echo the value + of the `Access-Control-Request-Method` request header. + + If the configuration contains the wildcard `*` in `allowMethods` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Methods` response header. Instead, it must + return a single HTTP method matching the value of the + `Access-Control-Request-Method` request header. + If the `Access-Control-Request-Method` header is not present in the request, + the gateway must omit the `Access-Control-Allow-Methods` response header. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `allowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + If the configuration contains the wildcard `*` in `allowOrigins` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Origin` + response header may either contain the wildcard `*` or echo the value + of the `Origin` request header. + + If the configuration contains the wildcard `*` in `allowOrigins` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Origin` response header. Instead, it must + return a single origin matching the value of the `Origin` request header. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `exposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. + + If the configuration contains the wildcard `*` in `exposeHeaders` and + `allowCredentials` is set to `false`, the `Access-Control-Expose-Headers` + response header can contain the wildcard `*`. + + If the configuration contains the wildcard `*` in `exposeHeaders` and + `allowCredentials` is set to `true`, the gateway cannot use the `*` + in the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal + to denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be + specified in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified + when type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified + when type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: statusCode 301 and 302 are Core; all others are Extended. + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + - CORS + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified + when type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified + when type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type + is not CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' + - message: filter.requestHeaderModifier must be nil + if the filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil + if the filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for + RequestMirror filter.type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the + filter.type is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != + ''RequestRedirect'')' + - message: filter.requestRedirect must be specified + for RequestRedirect filter.type + rule: '!(!has(self.requestRedirect) && self.type == + ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for + ExtensionRef filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: CORS filter cannot be repeated + rule: self.filter(f, f.type == 'CORS').size() <= 1 + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + - message: RequestRedirect filter cannot be repeated + rule: self.filter(f, f.type == 'RequestRedirect').size() + <= 1 + - message: URLRewrite filter cannot be repeated + rule: self.filter(f, f.type == 'URLRewrite').size() + <= 1 + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + filters: + description: |- + Filters define the filters that are applied to requests that match + this rule. + + Wherever possible, implementations SHOULD implement filters in the order + they are specified. + + Implementations MAY choose to implement this ordering strictly, rejecting + any combination or order of filters that cannot be supported. If implementations + choose a strict interpretation of filter ordering, they MUST clearly document + that behavior. + + To reject an invalid combination or order of filters, implementations SHOULD + consider the Route Rules with this configuration invalid. If all Route Rules + in a Route are invalid, the entire Route would be considered invalid. If only + a portion of Route Rules are invalid, implementations MUST set the + "PartiallyInvalid" condition for the Route. + + Conformance-levels at this level are defined based on the type of filter: + + - ALL core filters MUST be supported by all implementations. + - Implementers are encouraged to support extended filters. + - Implementation-specific custom filters have no API guarantees across + implementations. + + Specifying the same filter multiple times is not supported unless explicitly + indicated in the filter. + + All filters are expected to be compatible with each other except for the + URLRewrite and RequestRedirect filters, which may not be combined. If an + implementation cannot support other combinations of filters, they must clearly + document that limitation. In cases where incompatible or unsupported + filters are specified and cause the `Accepted` condition to be set to status + `False`, implementations may use the `IncompatibleFilters` reason to specify + this configuration error. + + Support: Core + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `allowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `allowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + + If the configuration contains the wildcard `*` in `allowHeaders` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Headers` + response header may either contain the wildcard `*` or echo the value + of the `Access-Control-Request-Headers` request header. + + If the configuration contains the wildcard `*` in `allowHeaders` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Headers` response header. Instead, it must + return one or more header names matching the value of the + `Access-Control-Request-Headers` request header. + If the `Access-Control-Request-Headers` header is not present in the + request, the gateway must omit the `Access-Control-Allow-Headers` + response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `allowMethods` field. + + When the `allowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `allowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If the configuration contains the wildcard `*` in `allowMethods` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Methods` + response header may either contain the wildcard `*` or echo the value + of the `Access-Control-Request-Method` request header. + + If the configuration contains the wildcard `*` in `allowMethods` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Methods` response header. Instead, it must + return a single HTTP method matching the value of the + `Access-Control-Request-Method` request header. + If the `Access-Control-Request-Method` header is not present in the request, + the gateway must omit the `Access-Control-Allow-Methods` response header. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `allowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + If the configuration contains the wildcard `*` in `allowOrigins` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Origin` + response header may either contain the wildcard `*` or echo the value + of the `Origin` request header. + + If the configuration contains the wildcard `*` in `allowOrigins` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Origin` response header. Instead, it must + return a single origin matching the value of the `Origin` request header. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `exposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. + + If the configuration contains the wildcard `*` in `exposeHeaders` and + `allowCredentials` is set to `false`, the `Access-Control-Expose-Headers` + response header can contain the wildcard `*`. + + If the configuration contains the wildcard `*` in `exposeHeaders` and + `allowCredentials` is set to `true`, the gateway cannot use the `*` + in the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to + denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be specified + in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: statusCode 301 and 302 are Core; all others are Extended. + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + - CORS + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type is not + CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' + - message: filter.requestHeaderModifier must be nil if the + filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil if the + filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for RequestMirror + filter.type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the filter.type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.requestRedirect must be specified for RequestRedirect + filter.type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for ExtensionRef + filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') && + self.exists(f, f.type == ''URLRewrite''))' + - message: CORS filter cannot be repeated + rule: self.filter(f, f.type == 'CORS').size() <= 1 + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + - message: RequestRedirect filter cannot be repeated + rule: self.filter(f, f.type == 'RequestRedirect').size() <= + 1 + - message: URLRewrite filter cannot be repeated + rule: self.filter(f, f.type == 'URLRewrite').size() <= 1 + matches: + default: + - path: + type: PathPrefix + value: / + description: |- + Matches define 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. + + For example, take the following matches configuration: + + ``` + matches: + - path: + value: "/foo" + headers: + - name: "version" + value: "v2" + - path: + value: "/v2/foo" + ``` + + For a request to match against this rule, a request must satisfy + EITHER of the two conditions: + + - path prefixed with `/foo` AND contains the header `version: v2` + - path prefix of `/v2/foo` + + See the documentation for HTTPRouteMatch on how to specify multiple + match conditions that should be ANDed together. + + If no matches are specified, the default is a prefix + path match on "/", which has the effect of matching every + HTTP request. + + Proxy or Load Balancer routing configuration generated from HTTPRoutes + MUST prioritize matches based on the following criteria, continuing on + ties. Across all rules specified on applicable Routes, precedence must be + given to the match having: + + * "Exact" path match. + * "Prefix" path match with largest number of characters. + * Method match. + * Largest number of header matches. + * Largest number of query param matches. + + Note: The precedence of RegularExpression path matches are implementation-specific. + + If ties still exist across multiple Routes, matching precedence MUST be + determined in order of the following criteria, continuing on ties: + + * The oldest Route based on creation timestamp. + * The Route appearing first in alphabetical order by + "{namespace}/{name}". + + If ties still exist within an HTTPRoute, matching precedence MUST be granted + to the FIRST matching rule (in list order) with a match meeting the above + criteria. + + When no rules matching a request have been successfully attached to the + parent a request is coming from, a HTTP 404 status code MUST be returned. + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given\naction. Multiple match types + are ANDed together, i.e. the match will\nevaluate to true + only if all conditions are satisfied.\n\nFor example, the + match below will match a HTTP request only if its path\nstarts + with `/foo` AND it contains the `version: v1` header:\n\n```\nmatch:\n\n\tpath:\n\t + \ value: \"/foo\"\n\theaders:\n\t- name: \"version\"\n\t + \ value \"v1\"\n\n```" + properties: + headers: + description: |- + Headers specifies HTTP request header matchers. Multiple match values are + ANDed together, meaning, a request must match all the specified headers + to select the route. + items: + description: |- + HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request + headers. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + + When a header is repeated in an HTTP request, it is + implementation-specific behavior as to how this is represented. + Generally, proxies should follow the guidance from the RFC: + https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 regarding + processing a repeated header, with special handling for "Set-Cookie". + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: |- + Type specifies how to match against the value of the header. + + Support: Core (Exact) + + Support: Implementation-specific (RegularExpression) + + Since RegularExpression HeaderMatchType has implementation-specific + conformance, implementations can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's documentation to + determine the supported dialect. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: |- + Method specifies HTTP method matcher. + When specified, this route will be matched only if the request has the + specified method. + + Support: Extended + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: |- + Path specifies a HTTP request path matcher. If this field is not + specified, a default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: |- + Type specifies how to match against the path Value. + + Support: Core (Exact, PathPrefix) + + Support: Implementation-specific (RegularExpression) + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + x-kubernetes-validations: + - message: value must be an absolute path and start with + '/' when type one of ['Exact', 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.startsWith(''/'') + : true' + - message: must not contain '//' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''//'') + : true' + - message: must not contain '/./' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/./'') + : true' + - message: must not contain '/../' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/../'') + : true' + - message: must not contain '%2f' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2f'') + : true' + - message: must not contain '%2F' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2F'') + : true' + - message: must not contain '#' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''#'') + : true' + - message: must not end with '/..' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/..'') + : true' + - message: must not end with '/.' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/.'') + : true' + - message: type must be one of ['Exact', 'PathPrefix', + 'RegularExpression'] + rule: self.type in ['Exact','PathPrefix'] || self.type + == 'RegularExpression' + - message: must only contain valid characters (matching + ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) + for types ['Exact', 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""") + : true' + queryParams: + description: |- + QueryParams specifies HTTP query parameter matchers. Multiple match + values are ANDed together, meaning, a request must match all the + specified query parameters to select the route. + + Support: Extended + items: + description: |- + HTTPQueryParamMatch describes how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: |- + Name is the name of the HTTP query param to be matched. This must be an + exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + + If multiple entries specify equivalent query param names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST be ignored. + + If a query param is repeated in an HTTP request, the behavior is + purposely left undefined, since different data planes have different + capabilities. However, it is *recommended* that implementations should + match against the first value of the param if the data plane supports it, + as this behavior is expected in other load balancing contexts outside of + the Gateway API. + + Users SHOULD NOT route traffic based on repeated query params to guard + themselves against potential differences in the implementations. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: |- + Type specifies how to match against the value of the query parameter. + + Support: Extended (Exact) + + Support: Implementation-specific (RegularExpression) + + Since RegularExpression QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, PCRE or any other + dialects of regular expressions. Please read the implementation's + documentation to determine the supported dialect. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + Name is the name of the route rule. This name MUST be unique within a Route if it is set. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + timeouts: + description: |- + Timeouts defines the timeouts that can be configured for an HTTP request. + + Support: Extended + properties: + backendRequest: + description: |- + BackendRequest specifies a timeout for an individual request from the gateway + to a backend. This covers the time from when the request first starts being + sent from the gateway to when the full response has been received from the backend. + + Setting a timeout to the zero duration (e.g. "0s") SHOULD disable the timeout + completely. Implementations that cannot completely disable the timeout MUST + instead interpret the zero duration as the longest possible value to which + the timeout can be set. + + An entire client HTTP transaction with a gateway, covered by the Request timeout, + may result in more than one call from the gateway to the destination backend, + for example, if automatic retries are supported. + + The value of BackendRequest must be a Gateway API Duration string as defined by + GEP-2257. When this field is unspecified, its behavior is implementation-specific; + when specified, the value of BackendRequest must be no more than the value of the + Request timeout (since the Request timeout encompasses the BackendRequest timeout). + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + request: + description: |- + Request specifies the maximum duration for a gateway to respond to an HTTP request. + If the gateway has not been able to respond before this deadline is met, the gateway + MUST return a timeout error. + + For example, setting the `rules.timeouts.request` field to the value `10s` in an + `HTTPRoute` will cause a timeout if a client request is taking longer than 10 seconds + to complete. + + Setting a timeout to the zero duration (e.g. "0s") SHOULD disable the timeout + completely. Implementations that cannot completely disable the timeout MUST + instead interpret the zero duration as the longest possible value to which + the timeout can be set. + + This timeout is intended to cover as close to the whole request-response transaction + as possible although an implementation MAY choose to start the timeout after the entire + request stream has been received instead of immediately after the transaction is + initiated by the client. + + The value of Request is a Gateway API Duration string as defined by GEP-2257. When this + field is unspecified, request timeout behavior is implementation-specific. + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + x-kubernetes-validations: + - message: backendRequest timeout cannot be longer than request + timeout + rule: '!(has(self.request) && has(self.backendRequest) && + duration(self.request) != duration(''0s'') && duration(self.backendRequest) + > duration(self.request))' + type: object + x-kubernetes-validations: + - message: RequestRedirect filter must not be used together with + backendRefs + rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ? + (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): + true' + - message: When using RequestRedirect filter with path.replacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + ? ((size(self.matches) != 1 || !has(self.matches[0].path) || + self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: When using URLRewrite filter with path.replacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + - message: Within backendRefs, when using RequestRedirect filter + with path.replacePrefixMatch, exactly one PathPrefix match must + be specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) + || self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: Within backendRefs, When using URLRewrite filter with + path.replacePrefixMatch, exactly one PathPrefix match must be + specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: While 16 rules and 64 matches per rule are allowed, the + total number of matches across all rules in a route must be less + than 128 + rule: '(self.size() > 0 ? self[0].matches.size() : 0) + (self.size() + > 1 ? self[1].matches.size() : 0) + (self.size() > 2 ? self[2].matches.size() + : 0) + (self.size() > 3 ? self[3].matches.size() : 0) + (self.size() + > 4 ? self[4].matches.size() : 0) + (self.size() > 5 ? self[5].matches.size() + : 0) + (self.size() > 6 ? self[6].matches.size() : 0) + (self.size() + > 7 ? self[7].matches.size() : 0) + (self.size() > 8 ? self[8].matches.size() + : 0) + (self.size() > 9 ? self[9].matches.size() : 0) + (self.size() + > 10 ? self[10].matches.size() : 0) + (self.size() > 11 ? self[11].matches.size() + : 0) + (self.size() > 12 ? self[12].matches.size() : 0) + (self.size() + > 13 ? self[13].matches.size() : 0) + (self.size() > 14 ? self[14].matches.size() + : 0) + (self.size() > 15 ? self[15].matches.size() : 0) <= 128' + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + HTTPRoute provides a way to route HTTP requests. This includes the capability + to match requests by hostname, path, header, or query param. Filters can be + used to specify additional processing steps. Backends specify where matching + requests should be routed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of hostnames that should match against the HTTP Host + header to select a HTTPRoute used to process the request. Implementations + MUST ignore any port value specified in the HTTP Host header while + performing a match and (absent of any applicable header modification + configuration) MUST forward this header unmodified to the backend. + + Valid values for Hostnames are determined by RFC 1123 definition of a + hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + If a hostname is specified by both the Listener and HTTPRoute, there + must be at least one intersecting hostname for the HTTPRoute to be + attached to the Listener. For example: + + * A Listener with `test.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `*.example.com`, `test.example.com`, and `foo.test.example.com` would + all match. On the other hand, `example.com` and `test.example.net` would + not match. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + If both the Listener and HTTPRoute have specified hostnames, any + HTTPRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + HTTPRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. + + If both the Listener and HTTPRoute have specified hostnames, and none + match with the criteria above, then the HTTPRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status of + `False` in the corresponding RouteParentStatus. + + In the event that multiple HTTPRoutes specify intersecting hostnames (e.g. + overlapping wildcard matching and exact matching hostnames), precedence must + be given to rules from the HTTPRoute with the largest number of: + + * Characters in a matching non-wildcard hostname. + * Characters in a matching hostname. + + If ties exist across multiple Routes, the matching precedence rules for + HTTPRouteMatches takes over. + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: |- + HTTPRouteRule defines semantics for matching an HTTP request based on + conditions (matches), processing it (filters), and forwarding the request to + an API object (backendRefs). + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. + + Failure behavior here depends on how many BackendRefs are specified and + how many are invalid. + + If *all* entries in BackendRefs are invalid, and there are also no filters + specified in this route rule, *all* traffic which matches this rule MUST + receive a 500 status code. + + See the HTTPBackendRef definition for the rules about what makes a single + HTTPBackendRef invalid. + + When a HTTPBackendRef is invalid, 500 status codes MUST be returned for + requests that would have otherwise been routed to an invalid backend. If + multiple backends are specified, and some are invalid, the proportion of + requests that would otherwise have been routed to an invalid backend + MUST receive a 500 status code. + + For example, if two backends are specified with equal weights, and one is + invalid, 50 percent of traffic must receive a 500. Implementations may + choose how that 50 percent is determined. + + When a HTTPBackendRef refers to a Service that has no ready endpoints, + implementations SHOULD return a 503 for requests to that backend instead. + If an implementation chooses to do this, all of the above rules for 500 responses + MUST also apply for responses that return a 503. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Core + items: + description: |- + HTTPBackendRef defines how a HTTPRoute forwards a HTTP request. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + properties: + filters: + description: |- + Filters defined at this level should be executed if and only if the + request is being forwarded to the backend defined here. + + Support: Implementation-specific (For broader support of filters, use the + Filters field in HTTPRouteRule.) + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `allowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `allowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + + If the configuration contains the wildcard `*` in `allowHeaders` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Headers` + response header may either contain the wildcard `*` or echo the value + of the `Access-Control-Request-Headers` request header. + + If the configuration contains the wildcard `*` in `allowHeaders` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Headers` response header. Instead, it must + return one or more header names matching the value of the + `Access-Control-Request-Headers` request header. + If the `Access-Control-Request-Headers` header is not present in the + request, the gateway must omit the `Access-Control-Allow-Headers` + response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `allowMethods` field. + + When the `allowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `allowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If the configuration contains the wildcard `*` in `allowMethods` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Methods` + response header may either contain the wildcard `*` or echo the value + of the `Access-Control-Request-Method` request header. + + If the configuration contains the wildcard `*` in `allowMethods` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Methods` response header. Instead, it must + return a single HTTP method matching the value of the + `Access-Control-Request-Method` request header. + If the `Access-Control-Request-Method` header is not present in the request, + the gateway must omit the `Access-Control-Allow-Methods` response header. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `allowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + If the configuration contains the wildcard `*` in `allowOrigins` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Origin` + response header may either contain the wildcard `*` or echo the value + of the `Origin` request header. + + If the configuration contains the wildcard `*` in `allowOrigins` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Origin` response header. Instead, it must + return a single origin matching the value of the `Origin` request header. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `exposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. + + If the configuration contains the wildcard `*` in `exposeHeaders` and + `allowCredentials` is set to `false`, the `Access-Control-Expose-Headers` + response header can contain the wildcard `*`. + + If the configuration contains the wildcard `*` in `exposeHeaders` and + `allowCredentials` is set to `true`, the gateway cannot use the `*` + in the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal + to denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be + specified in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified + when type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified + when type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: statusCode 301 and 302 are Core; all others are Extended. + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + - CORS + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified + when type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified + when type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type + is not CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' + - message: filter.requestHeaderModifier must be nil + if the filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil + if the filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for + RequestMirror filter.type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the + filter.type is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != + ''RequestRedirect'')' + - message: filter.requestRedirect must be specified + for RequestRedirect filter.type + rule: '!(!has(self.requestRedirect) && self.type == + ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for + ExtensionRef filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: CORS filter cannot be repeated + rule: self.filter(f, f.type == 'CORS').size() <= 1 + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + - message: RequestRedirect filter cannot be repeated + rule: self.filter(f, f.type == 'RequestRedirect').size() + <= 1 + - message: URLRewrite filter cannot be repeated + rule: self.filter(f, f.type == 'URLRewrite').size() + <= 1 + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + filters: + description: |- + Filters define the filters that are applied to requests that match + this rule. + + Wherever possible, implementations SHOULD implement filters in the order + they are specified. + + Implementations MAY choose to implement this ordering strictly, rejecting + any combination or order of filters that cannot be supported. If implementations + choose a strict interpretation of filter ordering, they MUST clearly document + that behavior. + + To reject an invalid combination or order of filters, implementations SHOULD + consider the Route Rules with this configuration invalid. If all Route Rules + in a Route are invalid, the entire Route would be considered invalid. If only + a portion of Route Rules are invalid, implementations MUST set the + "PartiallyInvalid" condition for the Route. + + Conformance-levels at this level are defined based on the type of filter: + + - ALL core filters MUST be supported by all implementations. + - Implementers are encouraged to support extended filters. + - Implementation-specific custom filters have no API guarantees across + implementations. + + Specifying the same filter multiple times is not supported unless explicitly + indicated in the filter. + + All filters are expected to be compatible with each other except for the + URLRewrite and RequestRedirect filters, which may not be combined. If an + implementation cannot support other combinations of filters, they must clearly + document that limitation. In cases where incompatible or unsupported + filters are specified and cause the `Accepted` condition to be set to status + `False`, implementations may use the `IncompatibleFilters` reason to specify + this configuration error. + + Support: Core + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `allowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `allowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + + If the configuration contains the wildcard `*` in `allowHeaders` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Headers` + response header may either contain the wildcard `*` or echo the value + of the `Access-Control-Request-Headers` request header. + + If the configuration contains the wildcard `*` in `allowHeaders` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Headers` response header. Instead, it must + return one or more header names matching the value of the + `Access-Control-Request-Headers` request header. + If the `Access-Control-Request-Headers` header is not present in the + request, the gateway must omit the `Access-Control-Allow-Headers` + response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `allowMethods` field. + + When the `allowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `allowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If the configuration contains the wildcard `*` in `allowMethods` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Methods` + response header may either contain the wildcard `*` or echo the value + of the `Access-Control-Request-Method` request header. + + If the configuration contains the wildcard `*` in `allowMethods` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Methods` response header. Instead, it must + return a single HTTP method matching the value of the + `Access-Control-Request-Method` request header. + If the `Access-Control-Request-Method` header is not present in the request, + the gateway must omit the `Access-Control-Allow-Methods` response header. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `://(:)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `allowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + If the configuration contains the wildcard `*` in `allowOrigins` and + `allowCredentials` is set to `false`, the `Access-Control-Allow-Origin` + response header may either contain the wildcard `*` or echo the value + of the `Origin` request header. + + If the configuration contains the wildcard `*` in `allowOrigins` and + `allowCredentials` is set to `true`, the gateway must not return `*` + in the `Access-Control-Allow-Origin` response header. Instead, it must + return a single origin matching the value of the `Origin` request header. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `exposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. + + If the configuration contains the wildcard `*` in `exposeHeaders` and + `allowCredentials` is set to `false`, the `Access-Control-Expose-Headers` + response header can contain the wildcard `*`. + + If the configuration contains the wildcard `*` in `exposeHeaders` and + `allowCredentials` is set to `true`, the gateway cannot use the `*` + in the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + + If the backend service requires TLS, use BackendTLSPolicy to tell the + implementation to supply the TLS details to be used to connect to that + backend. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to + denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be specified + in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: statusCode 301 and 302 are Core; all others are Extended. + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + - CORS + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type is not + CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' + - message: filter.requestHeaderModifier must be nil if the + filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil if the + filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for RequestMirror + filter.type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the filter.type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.requestRedirect must be specified for RequestRedirect + filter.type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for ExtensionRef + filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') && + self.exists(f, f.type == ''URLRewrite''))' + - message: CORS filter cannot be repeated + rule: self.filter(f, f.type == 'CORS').size() <= 1 + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + - message: RequestRedirect filter cannot be repeated + rule: self.filter(f, f.type == 'RequestRedirect').size() <= + 1 + - message: URLRewrite filter cannot be repeated + rule: self.filter(f, f.type == 'URLRewrite').size() <= 1 + matches: + default: + - path: + type: PathPrefix + value: / + description: |- + Matches define 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. + + For example, take the following matches configuration: + + ``` + matches: + - path: + value: "/foo" + headers: + - name: "version" + value: "v2" + - path: + value: "/v2/foo" + ``` + + For a request to match against this rule, a request must satisfy + EITHER of the two conditions: + + - path prefixed with `/foo` AND contains the header `version: v2` + - path prefix of `/v2/foo` + + See the documentation for HTTPRouteMatch on how to specify multiple + match conditions that should be ANDed together. + + If no matches are specified, the default is a prefix + path match on "/", which has the effect of matching every + HTTP request. + + Proxy or Load Balancer routing configuration generated from HTTPRoutes + MUST prioritize matches based on the following criteria, continuing on + ties. Across all rules specified on applicable Routes, precedence must be + given to the match having: + + * "Exact" path match. + * "Prefix" path match with largest number of characters. + * Method match. + * Largest number of header matches. + * Largest number of query param matches. + + Note: The precedence of RegularExpression path matches are implementation-specific. + + If ties still exist across multiple Routes, matching precedence MUST be + determined in order of the following criteria, continuing on ties: + + * The oldest Route based on creation timestamp. + * The Route appearing first in alphabetical order by + "{namespace}/{name}". + + If ties still exist within an HTTPRoute, matching precedence MUST be granted + to the FIRST matching rule (in list order) with a match meeting the above + criteria. + + When no rules matching a request have been successfully attached to the + parent a request is coming from, a HTTP 404 status code MUST be returned. + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given\naction. Multiple match types + are ANDed together, i.e. the match will\nevaluate to true + only if all conditions are satisfied.\n\nFor example, the + match below will match a HTTP request only if its path\nstarts + with `/foo` AND it contains the `version: v1` header:\n\n```\nmatch:\n\n\tpath:\n\t + \ value: \"/foo\"\n\theaders:\n\t- name: \"version\"\n\t + \ value \"v1\"\n\n```" + properties: + headers: + description: |- + Headers specifies HTTP request header matchers. Multiple match values are + ANDed together, meaning, a request must match all the specified headers + to select the route. + items: + description: |- + HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request + headers. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + + When a header is repeated in an HTTP request, it is + implementation-specific behavior as to how this is represented. + Generally, proxies should follow the guidance from the RFC: + https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 regarding + processing a repeated header, with special handling for "Set-Cookie". + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: |- + Type specifies how to match against the value of the header. + + Support: Core (Exact) + + Support: Implementation-specific (RegularExpression) + + Since RegularExpression HeaderMatchType has implementation-specific + conformance, implementations can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's documentation to + determine the supported dialect. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: |- + Method specifies HTTP method matcher. + When specified, this route will be matched only if the request has the + specified method. + + Support: Extended + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: |- + Path specifies a HTTP request path matcher. If this field is not + specified, a default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: |- + Type specifies how to match against the path Value. + + Support: Core (Exact, PathPrefix) + + Support: Implementation-specific (RegularExpression) + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + x-kubernetes-validations: + - message: value must be an absolute path and start with + '/' when type one of ['Exact', 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.startsWith(''/'') + : true' + - message: must not contain '//' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''//'') + : true' + - message: must not contain '/./' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/./'') + : true' + - message: must not contain '/../' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/../'') + : true' + - message: must not contain '%2f' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2f'') + : true' + - message: must not contain '%2F' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2F'') + : true' + - message: must not contain '#' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''#'') + : true' + - message: must not end with '/..' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/..'') + : true' + - message: must not end with '/.' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/.'') + : true' + - message: type must be one of ['Exact', 'PathPrefix', + 'RegularExpression'] + rule: self.type in ['Exact','PathPrefix'] || self.type + == 'RegularExpression' + - message: must only contain valid characters (matching + ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) + for types ['Exact', 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""") + : true' + queryParams: + description: |- + QueryParams specifies HTTP query parameter matchers. Multiple match + values are ANDed together, meaning, a request must match all the + specified query parameters to select the route. + + Support: Extended + items: + description: |- + HTTPQueryParamMatch describes how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: |- + Name is the name of the HTTP query param to be matched. This must be an + exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + + If multiple entries specify equivalent query param names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST be ignored. + + If a query param is repeated in an HTTP request, the behavior is + purposely left undefined, since different data planes have different + capabilities. However, it is *recommended* that implementations should + match against the first value of the param if the data plane supports it, + as this behavior is expected in other load balancing contexts outside of + the Gateway API. + + Users SHOULD NOT route traffic based on repeated query params to guard + themselves against potential differences in the implementations. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: |- + Type specifies how to match against the value of the query parameter. + + Support: Extended (Exact) + + Support: Implementation-specific (RegularExpression) + + Since RegularExpression QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, PCRE or any other + dialects of regular expressions. Please read the implementation's + documentation to determine the supported dialect. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + Name is the name of the route rule. This name MUST be unique within a Route if it is set. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + timeouts: + description: |- + Timeouts defines the timeouts that can be configured for an HTTP request. + + Support: Extended + properties: + backendRequest: + description: |- + BackendRequest specifies a timeout for an individual request from the gateway + to a backend. This covers the time from when the request first starts being + sent from the gateway to when the full response has been received from the backend. + + Setting a timeout to the zero duration (e.g. "0s") SHOULD disable the timeout + completely. Implementations that cannot completely disable the timeout MUST + instead interpret the zero duration as the longest possible value to which + the timeout can be set. + + An entire client HTTP transaction with a gateway, covered by the Request timeout, + may result in more than one call from the gateway to the destination backend, + for example, if automatic retries are supported. + + The value of BackendRequest must be a Gateway API Duration string as defined by + GEP-2257. When this field is unspecified, its behavior is implementation-specific; + when specified, the value of BackendRequest must be no more than the value of the + Request timeout (since the Request timeout encompasses the BackendRequest timeout). + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + request: + description: |- + Request specifies the maximum duration for a gateway to respond to an HTTP request. + If the gateway has not been able to respond before this deadline is met, the gateway + MUST return a timeout error. + + For example, setting the `rules.timeouts.request` field to the value `10s` in an + `HTTPRoute` will cause a timeout if a client request is taking longer than 10 seconds + to complete. + + Setting a timeout to the zero duration (e.g. "0s") SHOULD disable the timeout + completely. Implementations that cannot completely disable the timeout MUST + instead interpret the zero duration as the longest possible value to which + the timeout can be set. + + This timeout is intended to cover as close to the whole request-response transaction + as possible although an implementation MAY choose to start the timeout after the entire + request stream has been received instead of immediately after the transaction is + initiated by the client. + + The value of Request is a Gateway API Duration string as defined by GEP-2257. When this + field is unspecified, request timeout behavior is implementation-specific. + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + x-kubernetes-validations: + - message: backendRequest timeout cannot be longer than request + timeout + rule: '!(has(self.request) && has(self.backendRequest) && + duration(self.request) != duration(''0s'') && duration(self.backendRequest) + > duration(self.request))' + type: object + x-kubernetes-validations: + - message: RequestRedirect filter must not be used together with + backendRefs + rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ? + (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): + true' + - message: When using RequestRedirect filter with path.replacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + ? ((size(self.matches) != 1 || !has(self.matches[0].path) || + self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: When using URLRewrite filter with path.replacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + - message: Within backendRefs, when using RequestRedirect filter + with path.replacePrefixMatch, exactly one PathPrefix match must + be specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) + || self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: Within backendRefs, When using URLRewrite filter with + path.replacePrefixMatch, exactly one PathPrefix match must be + specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: While 16 rules and 64 matches per rule are allowed, the + total number of matches across all rules in a route must be less + than 128 + rule: '(self.size() > 0 ? self[0].matches.size() : 0) + (self.size() + > 1 ? self[1].matches.size() : 0) + (self.size() > 2 ? self[2].matches.size() + : 0) + (self.size() > 3 ? self[3].matches.size() : 0) + (self.size() + > 4 ? self[4].matches.size() : 0) + (self.size() > 5 ? self[5].matches.size() + : 0) + (self.size() > 6 ? self[6].matches.size() : 0) + (self.size() + > 7 ? self[7].matches.size() : 0) + (self.size() > 8 ? self[8].matches.size() + : 0) + (self.size() > 9 ? self[9].matches.size() : 0) + (self.size() + > 10 ? self[10].matches.size() : 0) + (self.size() > 11 ? self[11].matches.size() + : 0) + (self.size() > 12 ? self[12].matches.size() : 0) + (self.size() + > 13 ? self[13].matches.size() : 0) + (self.size() > 14 ? self[14].matches.size() + : 0) + (self.size() > 15 ? self[15].matches.size() : 0) <= 128' + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_listenersets.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: listenersets.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: ListenerSet + listKind: ListenerSetList + plural: listenersets + shortNames: + - lset + singular: listenerset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ListenerSet defines a set of additional listeners to attach to an existing Gateway. + This resource provides a mechanism to merge multiple listeners into a single Gateway. + + The parent Gateway must explicitly allow ListenerSet attachment through its + AllowedListeners configuration. By default, Gateways do not allow ListenerSet + attachment. + + Routes can attach to a ListenerSet by specifying it as a parentRef, and can + optionally target specific listeners using the sectionName field. + + Policy Attachment: + - Policies that attach to a ListenerSet apply to all listeners defined in that resource + - Policies do not impact listeners in the parent Gateway + - Different ListenerSets attached to the same Gateway can have different policies + - If an implementation cannot apply a policy to specific listeners, it should reject the policy + + ReferenceGrant Semantics: + - ReferenceGrants applied to a Gateway are not inherited by child ListenerSets + - ReferenceGrants applied to a ListenerSet do not grant permission to the parent Gateway's listeners + - A ListenerSet can reference secrets/backends in its own namespace without a ReferenceGrant + + Gateway Integration: + - The parent Gateway's status will include "AttachedListenerSets" + which is the count of ListenerSets that have successfully attached to a Gateway + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ListenerSet. + properties: + listeners: + description: |- + Listeners associated with this ListenerSet. Listeners define + logical endpoints that are bound on this referenced parent Gateway's addresses. + + Listeners in a `Gateway` and their attached `ListenerSets` are concatenated + as a list when programming the underlying infrastructure. Each listener + name does not need to be unique across the Gateway and ListenerSets. + See ListenerEntry.Name for more details. + + Implementations MUST treat the parent Gateway as having the merged + list of all listeners from itself and attached ListenerSets using + the following precedence: + + 1. "parent" Gateway + 2. ListenerSet ordered by creation time (oldest first) + 3. ListenerSet ordered alphabetically by "{namespace}/{name}". + + An implementation MAY reject listeners by setting the ListenerEntryStatus + `Accepted` condition to False with the Reason `TooManyListeners` + + If a listener has a conflict, this will be reported in the + Status.ListenerEntryStatus setting the `Conflicted` condition to True. + + Implementations SHOULD be cautious about what information from the + parent or siblings are reported to avoid accidentally leaking + sensitive information that the child would not otherwise have access + to. This can include contents of secrets etc. + items: + properties: + allowedRoutes: + default: + namespaces: + from: Same + description: |- + AllowedRoutes defines the types of routes that MAY be attached to a + Listener and the trusted namespaces where those Route resources MAY be + present. + + Although a client request may match multiple route rules, only one rule + may ultimately receive the request. Matching precedence MUST be + determined in order of the following criteria: + + * The most specific match as defined by the Route type. + * The oldest Route based on creation timestamp. For example, a Route with + a creation timestamp of "2020-09-08 01:02:03" is given precedence over + a Route with a creation timestamp of "2020-09-08 01:02:04". + * If everything else is equivalent, the Route appearing first in + alphabetical order (namespace/name) should be given precedence. For + example, foo/bar is given precedence over foo/baz. + + All valid rules within a Route attached to this Listener should be + implemented. Invalid Route rules can be ignored (sometimes that will mean + the full Route). If a Route rule transitions from valid to invalid, + support for that Route rule should be dropped to ensure consistency. For + example, even if a filter specified by a Route rule is invalid, the rest + of the rules within that Route should still be supported. + properties: + kinds: + description: |- + Kinds specifies the groups and kinds of Routes that are allowed to bind + to this Gateway Listener. When unspecified or empty, the kinds of Routes + selected are determined using the Listener protocol. + + A RouteGroupKind MUST correspond to kinds of Routes that are compatible + with the application protocol specified in the Listener's Protocol field. + If an implementation does not support or recognize this resource type, it + MUST set the "ResolvedRefs" condition to False for this Listener with the + "InvalidRouteKinds" reason. + + Support: Core + items: + description: RouteGroupKind indicates the group and kind + of a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + namespaces: + default: + from: Same + description: |- + Namespaces indicates namespaces from which Routes may be attached to this + Listener. This is restricted to the namespace of this Gateway by default. + + Support: Core + properties: + from: + default: Same + description: |- + From indicates where Routes will be selected for this Gateway. Possible + values are: + + * All: Routes in all namespaces may be used by this Gateway. + * Selector: Routes in namespaces selected by the selector may be used by + this Gateway. + * Same: Only Routes in the same namespace may be used by this Gateway. + + Support: Core + enum: + - All + - Selector + - Same + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only Routes in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + + Support: Core + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + hostname: + description: |- + Hostname specifies the virtual hostname to match for protocol types that + define this concept. When unspecified, all hostnames are matched. This + field is ignored for protocols that don't require hostname based + matching. + + Implementations MUST apply Hostname matching appropriately for each of + the following protocols: + + * TLS: The Listener Hostname MUST match the SNI. + * HTTP: The Listener Hostname MUST match the Host header of the request. + * HTTPS: The Listener Hostname SHOULD match at both the TLS and HTTP + protocol layers as described above. If an implementation does not + ensure that both the SNI and Host header match the Listener hostname, + it MUST clearly document that. + + For HTTPRoute and TLSRoute resources, there is an interaction with the + `spec.hostnames` array. When both listener and route specify hostnames, + there MUST be an intersection between the values for a Route to be + accepted. For more information, refer to the Route specific Hostnames + documentation. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + name: + description: |- + Name is the name of the Listener. This name MUST be unique within a + ListenerSet. + + Name is not required to be unique across a Gateway and ListenerSets. + Routes can attach to a Listener by having a ListenerSet as a parentRef + and setting the SectionName + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + port: + description: |- + Port is the network port. Multiple listeners may use the + same port, subject to the Listener compatibility rules. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: Protocol specifies the network protocol this listener + expects to receive. + maxLength: 255 + minLength: 1 + pattern: ^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9]+$ + type: string + tls: + description: |- + TLS is the TLS configuration for the Listener. This field is required if + the Protocol field is "HTTPS" or "TLS". It is invalid to set this field + if the Protocol field is "HTTP", "TCP", or "UDP". + + The association of SNIs to Certificate defined in ListenerTLSConfig is + defined based on the Hostname field for this listener. + + The GatewayClass MUST use the longest matching SNI out of all + available certificates for any TLS handshake. + properties: + certificateRefs: + description: |- + CertificateRefs contains a series of references to Kubernetes objects that + contains TLS certificates and private keys. These certificates are used to + establish a TLS handshake for requests that match the hostname of the + associated listener. + + A single CertificateRef to a Kubernetes Secret has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a Listener, but this behavior is implementation-specific. + + References to a resource in different namespace are invalid UNLESS there + is a ReferenceGrant in the target namespace that allows the certificate + to be attached. If a ReferenceGrant does not allow this reference, the + "ResolvedRefs" condition MUST be set to False for this listener with the + "RefNotPermitted" reason. + + This field is required to have at least one element when the mode is set + to "Terminate" (default) and is optional otherwise. + + CertificateRefs can reference to standard Kubernetes resources, i.e. + Secret, or implementation-specific custom resources. + + Support: Core - A single reference to a Kubernetes Secret of type kubernetes.io/tls + + Support: Implementation-specific (More than one reference or other resource types) + items: + description: |- + SecretObjectReference identifies an API object including its namespace, + defaulting to Secret. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + mode: + default: Terminate + description: |- + Mode defines the TLS behavior for the TLS session initiated by the client. + There are two possible modes: + + - Terminate: The TLS session between the downstream client and the + Gateway is terminated at the Gateway. This mode requires certificates + to be specified in some way, such as populating the certificateRefs + field. + - Passthrough: The TLS session is NOT terminated by the Gateway. This + implies that the Gateway can't decipher the TLS stream except for + the ClientHello message of the TLS protocol. The certificateRefs field + is ignored in this mode. + + Support: Core + enum: + - Terminate + - Passthrough + type: string + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + type: object + x-kubernetes-validations: + - message: certificateRefs or options must be specified when + mode is Terminate + rule: 'self.mode == ''Terminate'' ? size(self.certificateRefs) + > 0 || size(self.options) > 0 : true' + required: + - name + - port + - protocol + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: tls must not be specified for protocols ['HTTP', 'TCP', + 'UDP'] + rule: 'self.all(l, l.protocol in [''HTTP'', ''TCP'', ''UDP''] ? + !has(l.tls) : true)' + - message: tls mode must be Terminate for protocol HTTPS + rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode + == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' + - message: hostname must not be specified for protocols ['TCP', 'UDP'] + rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) + || l.hostname == '''') : true)' + - message: Listener name must be unique within the Gateway + rule: self.all(l1, self.exists_one(l2, l1.name == l2.name)) + - message: Combination of port, protocol and hostname must be unique + for each listener + rule: 'self.all(l1, !has(l1.port) || self.exists_one(l2, has(l2.port) + && l1.port == l2.port && l1.protocol == l2.protocol && (has(l1.hostname) + && has(l2.hostname) ? l1.hostname == l2.hostname : !has(l1.hostname) + && !has(l2.hostname))))' + parentRef: + description: ParentRef references the Gateway that the listeners are + attached to. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: Kind is kind of the referent. For example "Gateway". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. If not present, + the namespace of the referent is assumed to be the same as + the namespace of the referring object. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + required: + - listeners + - parentRef + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: Status defines the current state of ListenerSet. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: |- + Conditions describe the current conditions of the ListenerSet. + + Implementations MUST express ListenerSet conditions using the + `ListenerSetConditionType` and `ListenerSetConditionReason` + constants so that operators and tools can converge on a common + vocabulary to describe ListenerSet state. + + Known condition types are: + + * "Accepted" + * "Programmed" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + listeners: + description: Listeners provide status for each unique listener port + defined in the Spec. + items: + description: ListenerStatus is the status associated with a Listener. + properties: + attachedRoutes: + description: |- + AttachedRoutes represents the total number of Routes that have been + successfully attached to this Listener. + + Successful attachment of a Route to a Listener is based solely on the + combination of the AllowedRoutes field on the corresponding Listener + and the Route's ParentRefs field. A Route is successfully attached to + a Listener when it is selected by the Listener's AllowedRoutes field + AND the Route has a valid ParentRef selecting the whole Gateway + resource or a specific Listener as a parent resource (more detail on + attachment semantics can be found in the documentation on the various + Route kinds ParentRefs fields). Listener status does not impact + successful attachment, i.e. the AttachedRoutes field count MUST be set + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. + + Uses for this field include troubleshooting Route attachment and + measuring blast radius/impact of changes to a Listener. + format: int32 + type: integer + conditions: + description: Conditions describe the current condition of this + listener. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: Name is the name of the Listener that this status + corresponds to. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + supportedKinds: + description: |- + SupportedKinds is the list indicating the Kinds supported by this + listener. This MUST represent the kinds supported by an implementation for + that Listener configuration. + + If kinds are specified in Spec that are not supported, they MUST NOT + appear in this list and an implementation MUST set the "ResolvedRefs" + condition to "False" with the "InvalidRouteKinds" reason. If both valid + and invalid Route kinds are specified, the implementation MUST + reference the valid Route kinds that have been specified. + items: + description: RouteGroupKind indicates the group and kind of + a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + required: + - attachedRoutes + - conditions + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_referencegrants.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: referencegrants.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: ReferenceGrant + listKind: ReferenceGrantList + plural: referencegrants + shortNames: + - refgrant + singular: referencegrant + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ReferenceGrant identifies kinds of resources in other namespaces that are + trusted to reference the specified kinds of resources in the same namespace + as the policy. + + Each ReferenceGrant can be used to represent a unique trust relationship. + Additional Reference Grants can be used to add to the set of trusted + sources of inbound references for the namespace they are defined within. + + All cross-namespace references in Gateway API (with the exception of cross-namespace + Gateway-route attachment) require a ReferenceGrant. + + ReferenceGrant is a form of runtime verification allowing users to assert + which cross-namespace object references are permitted. Implementations that + support ReferenceGrant MUST NOT permit cross-namespace references which have + no grant, and MUST respond to the removal of a grant by revoking the access + that the grant allowed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ReferenceGrant. + properties: + from: + description: |- + From describes the trusted namespaces and kinds that can reference the + resources described in "To". Each entry in this list MUST be considered + to be an additional place that references can be valid from, or to put + this another way, entries MUST be combined using OR. + + Support: Core + items: + description: ReferenceGrantFrom describes trusted namespaces and + kinds. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field. + + When used to permit a SecretObjectReference: + + * Gateway + + When used to permit a BackendObjectReference: + + * GRPCRoute + * HTTPRoute + * TCPRoute + * TLSRoute + * UDPRoute + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - namespace + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + to: + description: |- + To describes the resources that may be referenced by the resources + described in "From". Each entry in this list MUST be considered to be an + additional place that references can be valid to, or to put this another + way, entries MUST be combined using OR. + + Support: Core + items: + description: |- + ReferenceGrantTo describes what Kinds are allowed as targets of the + references. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field: + + * Secret when used to permit a SecretObjectReference + * Service when used to permit a BackendObjectReference + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. When unspecified, this policy + refers to all resources of the specified Group and Kind in the local + namespace. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - from + - to + type: object + required: + - spec + type: object + served: true + storage: false + subresources: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + ReferenceGrant identifies kinds of resources in other namespaces that are + trusted to reference the specified kinds of resources in the same namespace + as the policy. + + Each ReferenceGrant can be used to represent a unique trust relationship. + Additional Reference Grants can be used to add to the set of trusted + sources of inbound references for the namespace they are defined within. + + All cross-namespace references in Gateway API (with the exception of cross-namespace + Gateway-route attachment) require a ReferenceGrant. + + ReferenceGrant is a form of runtime verification allowing users to assert + which cross-namespace object references are permitted. Implementations that + support ReferenceGrant MUST NOT permit cross-namespace references which have + no grant, and MUST respond to the removal of a grant by revoking the access + that the grant allowed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ReferenceGrant. + properties: + from: + description: |- + From describes the trusted namespaces and kinds that can reference the + resources described in "To". Each entry in this list MUST be considered + to be an additional place that references can be valid from, or to put + this another way, entries MUST be combined using OR. + + Support: Core + items: + description: ReferenceGrantFrom describes trusted namespaces and + kinds. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field. + + When used to permit a SecretObjectReference: + + * Gateway + + When used to permit a BackendObjectReference: + + * GRPCRoute + * HTTPRoute + * TCPRoute + * TLSRoute + * UDPRoute + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - namespace + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + to: + description: |- + To describes the resources that may be referenced by the resources + described in "From". Each entry in this list MUST be considered to be an + additional place that references can be valid to, or to put this another + way, entries MUST be combined using OR. + + Support: Core + items: + description: |- + ReferenceGrantTo describes what Kinds are allowed as targets of the + references. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field: + + * Secret when used to permit a SecretObjectReference + * Service when used to permit a BackendObjectReference + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. When unspecified, this policy + refers to all resources of the specified Group and Kind in the local + namespace. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - from + - to + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_tcproutes.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: tcproutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: TCPRoute + listKind: TCPRouteList + plural: tcproutes + singular: tcproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + TCPRoute provides a way to route TCP requests. When combined with a Gateway + listener, it can be used to forward connections on the port specified by the + listener to a set of backends specified by the TCPRoute. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TCPRoute. + properties: + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of TCP matchers and actions. + items: + description: TCPRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or a + Service with no endpoints), the underlying implementation MUST actively + reject connection attempts to this backend. Connection rejections must + respect weight; if an invalid backend is requested to have 80% of + connections, then 80% of connections must be rejected instead. + + Support: Core for Kubernetes Service + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 1 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + status: + description: Status defines the current state of TCPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha2 version of TCPRoute has been deprecated and will + be removed in a future release of the API. Please upgrade to v1. + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + TCPRoute provides a way to route TCP requests. When combined with a Gateway + listener, it can be used to forward connections on the port specified by the + listener to a set of backends specified by the TCPRoute. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TCPRoute. + properties: + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of TCP matchers and actions. + items: + description: TCPRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or a + Service with no endpoints), the underlying implementation MUST actively + reject connection attempts to this backend. Connection rejections must + respect weight; if an invalid backend is requested to have 80% of + connections, then 80% of connections must be rejected instead. + + Support: Core for Kubernetes Service + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + status: + description: Status defines the current state of TCPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_tlsroutes.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: tlsroutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: TLSRoute + listKind: TLSRouteList + plural: tlsroutes + singular: tlsroute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility + in matching streams for a given TLS listener. + + If you need to forward traffic to a single target for a TLS listener, you + could choose to use a TCPRoute with a TLS listener. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of SNI hostnames that should match against the + SNI attribute of TLS ClientHello message in TLS handshake. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed in SNI hostnames per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 1024 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: Hostnames cannot contain an IP + rule: self.all(h, !isIP(h)) + - message: Hostnames must be valid based on RFC-1123 + rule: 'self.all(h, !h.contains(''*'') ? h.matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'') + : true)' + - message: Wildcards on hostnames must be the first label, and the + rest of hostname must be valid based on RFC-1123 + rule: 'self.all(h, h.contains(''*'') ? (h.startsWith(''*.'') && + h.substring(2).matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'')) + : true)' + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or + a Service with no endpoints), the rule performs no forwarding; if no + filters are specified that would result in a response being sent, the + underlying implementation must actively reject request attempts to this + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. + + When a TLSRoute is attached to a listener in Terminate mode, a BackendTLSPolicy + can be used to enable re-encryption of the traffic to the backends. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Extended + + Support for BackendTLSPolicy: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 1 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - hostnames + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha2 version of TLSRoute has been deprecated and will + be removed in a future release of the API. Please upgrade to v1. + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility + in matching streams for a given TLS listener. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of SNI names that should match against the + SNI attribute of TLS ClientHello message in TLS handshake. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed in SNI names per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + If a hostname is specified by both the Listener and TLSRoute, there + must be at least one intersecting hostname for the TLSRoute to be + attached to the Listener. For example: + + * A Listener with `test.example.com` as the hostname matches TLSRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches TLSRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `test.example.com` and `*.example.com` would both match. On the other + hand, `example.com` and `test.example.net` would not match. + + If both the Listener and TLSRoute have specified hostnames, any + TLSRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + TLSRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. + + If both the Listener and TLSRoute have specified hostnames, and none + match with the criteria above, then the TLSRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status of + `False` in the corresponding RouteParentStatus. + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 1024 + type: array + x-kubernetes-list-type: atomic + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of TLS matchers and actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or + a Service with no endpoints), the rule performs no forwarding; if no + filters are specified that would result in a response being sent, the + underlying implementation must actively reject request attempts to this + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. + + When a TLSRoute is attached to a listener in Terminate mode, a BackendTLSPolicy + can be used to enable re-encryption of the traffic to the backends. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Extended + + Support for BackendTLSPolicy: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha3 version of TLSRoute has been deprecated and will + be removed in a future release of the API. Please upgrade to v1. + name: v1alpha3 + schema: + openAPIV3Schema: + description: |- + The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility + in matching streams for a given TLS listener. + + If you need to forward traffic to a single target for a TLS listener, you + could choose to use a TCPRoute with a TLS listener. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of SNI hostnames that should match against the + SNI attribute of TLS ClientHello message in TLS handshake. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed in SNI hostnames per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 1024 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: Hostnames cannot contain an IP + rule: self.all(h, !isIP(h)) + - message: Hostnames must be valid based on RFC-1123 + rule: 'self.all(h, !h.contains(''*'') ? h.matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'') + : true)' + - message: Wildcards on hostnames must be the first label, and the + rest of hostname must be valid based on RFC-1123 + rule: 'self.all(h, h.contains(''*'') ? (h.startsWith(''*.'') && + h.substring(2).matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'')) + : true)' + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or + a Service with no endpoints), the rule performs no forwarding; if no + filters are specified that would result in a response being sent, the + underlying implementation must actively reject request attempts to this + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. + + When a TLSRoute is attached to a listener in Terminate mode, a BackendTLSPolicy + can be used to enable re-encryption of the traffic to the backends. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Extended + + Support for BackendTLSPolicy: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 1 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - hostnames + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_udproutes.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: udproutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: UDPRoute + listKind: UDPRouteList + plural: udproutes + singular: udproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + UDPRoute provides a way to route UDP traffic. When combined with a Gateway + listener, it can be used to forward traffic on the port specified by the + listener to a set of backends specified by the UDPRoute. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of UDPRoute. + properties: + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of UDP matchers and actions. + items: + description: UDPRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or a + Service with no endpoints), the underlying implementation MUST actively + reject connection attempts to this backend. Packet drops must + respect weight; if an invalid backend is requested to have 80% of + the packets, then 80% of packets must be dropped instead. + + Support: Extended for Kubernetes Service + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 1 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + status: + description: Status defines the current state of UDPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha2 version of UDPRoute has been deprecated and will + be removed in a future release of the API. Please upgrade to v1. + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + UDPRoute provides a way to route UDP traffic. When combined with a Gateway + listener, it can be used to forward traffic on the port specified by the + listener to a set of backends specified by the UDPRoute. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of UDPRoute. + properties: + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of UDP matchers and actions. + items: + description: UDPRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or a + Service with no endpoints), the underlying implementation MUST actively + reject connection attempts to this backend. Packet drops must + respect weight; if an invalid backend is requested to have 80% of + the packets, then 80% of packets must be dropped instead. + + Support: Extended for Kubernetes Service + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + status: + description: Status defines the current state of UDPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} +--- +# +# config/crd/standard/gateway.networking.k8s.io_vap_safeupgrades.yaml +# +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + annotations: + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: "safe-upgrades.gateway.networking.k8s.io" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apiextensions.k8s.io"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["*"] + validations: + - expression: "object.spec.group != 'gateway.networking.k8s.io' || oldObject == null || ( + has(object.metadata.annotations) && object.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/channel') && + object.metadata.annotations['gateway.networking.k8s.io/channel'] == 'standard' ) || ( + oldObject != null && has(oldObject.metadata.annotations) && oldObject.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/channel') && + oldObject.metadata.annotations['gateway.networking.k8s.io/channel'] == 'experimental' )" + message: "Installing experimental CRDs on top of standard channel CRDs is prohibited by default. Uninstall ValidatingAdmissionPolicy safe-upgrades.gateway.networking.k8s.io to install experimental CRDs on top of standard channel CRDs." + reason: Invalid + - expression: "object.spec.group != 'gateway.networking.k8s.io' || + (has(object.metadata.annotations) && object.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/bundle-version') && + ( + matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], '-(rc)') || + ( + !matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], 'v1.[0-5].\\\\d+') && + !matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], 'v0') + ) + ))" #TODO Kubernetes 1.37: Migrate to kubernetes semver library + message: "Installing CRDs with version before v1.5.0 is prohibited by default. Uninstall ValidatingAdmissionPolicy safe-upgrades.gateway.networking.k8s.io to install older versions." + reason: Invalid + +--- + +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + annotations: + gateway.networking.k8s.io/bundle-version: v1.6.3 + gateway.networking.k8s.io/channel: standard + name: safe-upgrades.gateway.networking.k8s.io +spec: + policyName: safe-upgrades.gateway.networking.k8s.io + validationActions: [Deny] + matchResources: + resourceRules: + - apiGroups: ["apiextensions.k8s.io"] + apiVersions: ["v1"] + resources: ["customresourcedefinitions"] + operations: ["CREATE", "UPDATE"] diff --git a/charts/gateway-crds/templates/traefik.yaml b/charts/gateway-crds/templates/traefik.yaml new file mode 100644 index 0000000..8ccd1b5 --- /dev/null +++ b/charts/gateway-crds/templates/traefik.yaml @@ -0,0 +1,4615 @@ +# Traefik's own CRDs, from the crds/ of the traefik chart 42.1.3 (Traefik v3.7.16), with +# helm.sh/resource-policy: keep. Helm never upgrades a chart's crds/, so they are +# templates here; the traefik chart's install then finds them and skips its copies. +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: ingressroutes.traefik.io +spec: + group: traefik.io + names: + kind: IngressRoute + listKind: IngressRouteList + plural: ingressroutes + singular: ingressroute + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IngressRoute is the CRD implementation of a Traefik HTTP Router. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: IngressRouteSpec defines the desired state of IngressRoute. + properties: + entryPoints: + description: |- + EntryPoints defines the list of entry point names to bind to. + Entry points have to be configured in the static configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/install-configuration/entrypoints/ + Default: all. + items: + type: string + type: array + ingressClassName: + description: IngressClassName defines the name of the IngressClass + cluster resource. + type: string + parentRefs: + description: |- + ParentRefs defines references to parent IngressRoute resources for multi-layer routing. + When set, this IngressRoute's routers will be children of the referenced parent IngressRoute's routers. + More info: https://doc.traefik.io/traefik/v3.7/routing/routers/#parentrefs + items: + description: IngressRouteRef is a reference to an IngressRoute resource. + properties: + name: + description: Name defines the name of the referenced IngressRoute + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + IngressRoute resource. + type: string + required: + - name + type: object + type: array + routes: + description: Routes defines the list of routes. + items: + description: Route holds the HTTP route configuration. + properties: + kind: + description: |- + Kind defines the kind of the route. + Rule is the only supported kind. + If not defined, defaults to Rule. + enum: + - Rule + type: string + match: + description: |- + Match defines the router's rule. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/rules-and-priority/ + type: string + middlewares: + description: |- + Middlewares defines the list of references to Middleware resources. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/kubernetes/crd/http/middleware/ + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array + observability: + description: |- + Observability defines the observability configuration for a router. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/observability/ + properties: + accessLogs: + description: AccessLogs enables access logs for this router. + type: boolean + metrics: + description: Metrics enables metrics for this router. + type: boolean + traceVerbosity: + default: minimal + description: TraceVerbosity defines the verbosity level + of the tracing for this router. + enum: + - minimal + - detailed + type: string + tracing: + description: Tracing enables tracing for this router. + type: boolean + type: object + priority: + description: |- + Priority defines the router's priority. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/rules-and-priority/#priority + maximum: 9223372036854775000 + type: integer + services: + description: |- + Services defines the list of Service. + It can contain any combination of TraefikService and/or reference to a Kubernetes Service. + items: + description: Service defines an upstream HTTP service to proxy + traffic to. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be + sent to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for + the health check endpoint. + type: string + port: + description: Port defines the server URL port for + the health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme + for the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of + the referenced Middleware resource. + type: string + required: + - name + type: object + type: array + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health + checks for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window + during which the failed attempts must occur for + the server to be marked as unhealthy. It also defines + for how long the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window + before marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to + the client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/service/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie + can be accessed by client-side APIs, such as + JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie + can only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - leasttime + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + type: array + syntax: + description: |- + Syntax defines the router's rule syntax. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/rules-and-priority/#rulesyntax + + Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. + type: string + required: + - match + type: object + type: array + tls: + description: |- + TLS defines the TLS configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/routing/router/#tls + properties: + certResolver: + description: |- + CertResolver defines the name of the certificate resolver to use. + Cert resolvers have to be configured in the static configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/install-configuration/tls/certificate-resolvers/acme/ + type: string + domains: + description: |- + Domains defines the list of domains that will be used to issue certificates. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/tls/tls-certificates/#domains + items: + description: Domain holds a domain name with SANs. + properties: + main: + description: Main defines the main domain name. + type: string + sans: + description: SANs defines the subject alternative domain + names. + items: + type: string + type: array + type: object + type: array + options: + description: |- + Options defines the reference to a TLSOption, that specifies the parameters of the TLS connection. + If not defined, the `default` TLSOption is used. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/tls/tls-options/ + properties: + name: + description: |- + Name defines the name of the referenced TLSOption. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/kubernetes/crd/http/tlsoption/ + type: string + namespace: + description: |- + Namespace defines the namespace of the referenced TLSOption. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/kubernetes/crd/http/tlsoption/ + type: string + required: + - name + type: object + secretName: + description: SecretName is the name of the referenced Kubernetes + Secret to specify the certificate details. + type: string + store: + description: |- + Store defines the reference to the TLSStore, that will be used to store certificates. + Please note that only `default` TLSStore can be used. + properties: + name: + description: |- + Name defines the name of the referenced TLSStore. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/kubernetes/crd/http/tlsstore/ + type: string + namespace: + description: |- + Namespace defines the namespace of the referenced TLSStore. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/kubernetes/crd/http/tlsstore/ + type: string + required: + - name + type: object + type: object + required: + - routes + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: ingressroutetcps.traefik.io +spec: + group: traefik.io + names: + kind: IngressRouteTCP + listKind: IngressRouteTCPList + plural: ingressroutetcps + singular: ingressroutetcp + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IngressRouteTCP is the CRD implementation of a Traefik TCP Router. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: IngressRouteTCPSpec defines the desired state of IngressRouteTCP. + properties: + entryPoints: + description: |- + EntryPoints defines the list of entry point names to bind to. + Entry points have to be configured in the static configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/install-configuration/entrypoints/ + Default: all. + items: + type: string + type: array + ingressClassName: + description: IngressClassName defines the name of the IngressClass + cluster resource. + type: string + routes: + description: Routes defines the list of routes. + items: + description: RouteTCP holds the TCP route configuration. + properties: + match: + description: |- + Match defines the router's rule. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/routing/rules-and-priority/ + type: string + middlewares: + description: Middlewares defines the list of references to MiddlewareTCP + resources. + items: + description: ObjectReference is a generic reference to a Traefik + resource. + properties: + name: + description: Name defines the name of the referenced Traefik + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Traefik resource. + type: string + required: + - name + type: object + type: array + priority: + description: |- + Priority defines the router's priority. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/routing/rules-and-priority/#priority + maximum: 9223372036854775000 + type: integer + services: + description: Services defines the list of TCP services. + items: + description: ServiceTCP defines an upstream TCP service to + proxy traffic to. + properties: + name: + description: Name defines the name of the referenced Kubernetes + Service. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + proxyProtocol: + description: |- + ProxyProtocol defines the PROXY protocol configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/service/#proxy-protocol + + Deprecated: ProxyProtocol will not be supported in future APIVersions, please use ServersTransport to configure ProxyProtocol instead. + properties: + version: + description: Version defines the PROXY Protocol version + to use. + maximum: 2 + minimum: 1 + type: integer + type: object + serversTransport: + description: |- + ServersTransport defines the name of ServersTransportTCP resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + terminationDelay: + description: |- + TerminationDelay defines the deadline that the proxy sets, after one of its connected peers indicates + it has closed the writing capability of its connection, to close the reading capability as well, + hence fully terminating the connection. + It is a duration in milliseconds, defaulting to 100. + A negative value means an infinite deadline (i.e. the reading capability is never closed). + + Deprecated: TerminationDelay will not be supported in future APIVersions, please use ServersTransport to configure the TerminationDelay instead. + type: integer + tls: + description: TLS determines whether to use TLS when dialing + with the backend. + type: boolean + weight: + description: Weight defines the weight used when balancing + requests between multiple Kubernetes Service. + minimum: 0 + type: integer + required: + - name + - port + type: object + type: array + syntax: + description: |- + Syntax defines the router's rule syntax. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/routing/rules-and-priority/#rulesyntax + + Deprecated: Please do not use this field and rewrite the router rules to use the v3 syntax. + enum: + - v3 + - v2 + type: string + required: + - match + type: object + type: array + tls: + description: |- + TLS defines the TLS configuration on a layer 4 / TCP Route. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/routing/router/#tls + properties: + certResolver: + description: |- + CertResolver defines the name of the certificate resolver to use. + Cert resolvers have to be configured in the static configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/install-configuration/tls/certificate-resolvers/acme/ + type: string + domains: + description: |- + Domains defines the list of domains that will be used to issue certificates. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/tls/#domains + items: + description: Domain holds a domain name with SANs. + properties: + main: + description: Main defines the main domain name. + type: string + sans: + description: SANs defines the subject alternative domain + names. + items: + type: string + type: array + type: object + type: array + options: + description: |- + Options defines the reference to a TLSOption, that specifies the parameters of the TLS connection. + If not defined, the `default` TLSOption is used. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/tls/#tls-options + properties: + name: + description: Name defines the name of the referenced Traefik + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Traefik resource. + type: string + required: + - name + type: object + passthrough: + description: Passthrough defines whether a TLS router will terminate + the TLS connection. + type: boolean + secretName: + description: SecretName is the name of the referenced Kubernetes + Secret to specify the certificate details. + type: string + store: + description: |- + Store defines the reference to the TLSStore, that will be used to store certificates. + Please note that only `default` TLSStore can be used. + properties: + name: + description: Name defines the name of the referenced Traefik + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Traefik resource. + type: string + required: + - name + type: object + type: object + required: + - routes + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: ingressrouteudps.traefik.io +spec: + group: traefik.io + names: + kind: IngressRouteUDP + listKind: IngressRouteUDPList + plural: ingressrouteudps + singular: ingressrouteudp + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: IngressRouteUDP is a CRD implementation of a Traefik UDP Router. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: IngressRouteUDPSpec defines the desired state of a IngressRouteUDP. + properties: + entryPoints: + description: |- + EntryPoints defines the list of entry point names to bind to. + Entry points have to be configured in the static configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/install-configuration/entrypoints/ + Default: all. + items: + type: string + type: array + ingressClassName: + description: IngressClassName defines the name of the IngressClass + cluster resource. + type: string + routes: + description: Routes defines the list of routes. + items: + description: RouteUDP holds the UDP route configuration. + properties: + services: + description: Services defines the list of UDP services. + items: + description: ServiceUDP defines an upstream UDP service to + proxy traffic to. + properties: + name: + description: Name defines the name of the referenced Kubernetes + Service. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + weight: + description: Weight defines the weight used when balancing + requests between multiple Kubernetes Service. + minimum: 0 + type: integer + required: + - name + - port + type: object + type: array + type: object + type: array + required: + - routes + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: middlewares.traefik.io +spec: + group: traefik.io + names: + kind: Middleware + listKind: MiddlewareList + plural: middlewares + singular: middleware + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + Middleware is the CRD implementation of a Traefik Middleware. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/overview/ + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: MiddlewareSpec defines the desired state of a Middleware. + properties: + addPrefix: + description: |- + AddPrefix holds the add prefix middleware configuration. + This middleware updates the path of a request before forwarding it. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/addprefix/ + properties: + prefix: + description: |- + Prefix is the string to add before the current path in the requested URL. + It should include a leading slash (/). + type: string + x-kubernetes-validations: + - message: must start with a '/' + rule: self.startsWith('/') + type: object + basicAuth: + description: |- + BasicAuth holds the basic auth middleware configuration. + This middleware restricts access to your services to known users. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/basicauth/ + properties: + headerField: + description: |- + HeaderField defines a header field to store the authenticated user. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/basicauth/#headerfield + type: string + realm: + description: |- + Realm allows the protected resources on a server to be partitioned into a set of protection spaces, each with its own authentication scheme. + Default: traefik. + type: string + removeHeader: + description: |- + RemoveHeader sets the removeHeader option to true to remove the authorization header before forwarding the request to your service. + Default: false. + type: boolean + secret: + description: Secret is the name of the referenced Kubernetes Secret + containing user credentials. + type: string + type: object + buffering: + description: |- + Buffering holds the buffering middleware configuration. + This middleware retries or limits the size of requests that can be forwarded to backends. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/buffering/#maxrequestbodybytes + properties: + maxRequestBodyBytes: + description: |- + MaxRequestBodyBytes defines the maximum allowed body size for the request (in bytes). + If the request exceeds the allowed size, it is not forwarded to the service, and the client gets a 413 (Request Entity Too Large) response. + Default: 0 (no maximum). + format: int64 + type: integer + maxResponseBodyBytes: + description: |- + MaxResponseBodyBytes defines the maximum allowed response size from the service (in bytes). + If the response exceeds the allowed size, it is not forwarded to the client. The client gets a 500 (Internal Server Error) response instead. + Default: 0 (no maximum). + format: int64 + type: integer + memRequestBodyBytes: + description: |- + MemRequestBodyBytes defines the threshold (in bytes) from which the request will be buffered on disk instead of in memory. + Default: 1048576 (1Mi). + format: int64 + type: integer + memResponseBodyBytes: + description: |- + MemResponseBodyBytes defines the threshold (in bytes) from which the response will be buffered on disk instead of in memory. + Default: 1048576 (1Mi). + format: int64 + type: integer + retryExpression: + description: |- + RetryExpression defines the retry conditions. + It is a logical combination of functions with operators AND (&&) and OR (||). + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/buffering/#retryexpression + type: string + type: object + chain: + description: |- + Chain holds the configuration of the chain middleware. + This middleware enables to define reusable combinations of other pieces of middleware. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/chain/ + properties: + middlewares: + description: Middlewares is the list of MiddlewareRef which composes + the chain. + items: + description: MiddlewareRef is a reference to a Middleware resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array + type: object + circuitBreaker: + description: CircuitBreaker holds the circuit breaker configuration. + properties: + checkPeriod: + anyOf: + - type: integer + - type: string + description: CheckPeriod is the interval between successive checks + of the circuit breaker condition (when in standby state). + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + expression: + description: Expression is the condition that triggers the tripped + state. + type: string + fallbackDuration: + anyOf: + - type: integer + - type: string + description: FallbackDuration is the duration for which the circuit + breaker will wait before trying to recover (from a tripped state). + x-kubernetes-int-or-string: true + recoveryDuration: + anyOf: + - type: integer + - type: string + description: RecoveryDuration is the duration for which the circuit + breaker will try to recover (as soon as it is in recovering + state). + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + responseCode: + description: ResponseCode is the status code that the circuit + breaker will return while it is in the open state. + maximum: 599 + minimum: 100 + type: integer + type: object + compress: + description: |- + Compress holds the compress middleware configuration. + This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/compress/ + properties: + defaultEncoding: + description: DefaultEncoding specifies the default encoding if + the `Accept-Encoding` header is not in the request or contains + a wildcard (`*`). + type: string + encodings: + description: Encodings defines the list of supported compression + algorithms. + items: + type: string + type: array + excludedContentTypes: + description: |- + ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. + `application/grpc` is always excluded. + items: + type: string + type: array + includedContentTypes: + description: IncludedContentTypes defines the list of content + types to compare the Content-Type header of the responses before + compressing. + items: + type: string + type: array + minResponseBodyBytes: + description: |- + MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed. + Default: 1024. + minimum: 0 + type: integer + type: object + contentType: + description: |- + ContentType holds the content-type middleware configuration. + This middleware exists to enable the correct behavior until at least the default one can be changed in a future version. + properties: + autoDetect: + description: |- + AutoDetect specifies whether to let the `Content-Type` header, if it has not been set by the backend, + be automatically set to a value derived from the contents of the response. + + Deprecated: AutoDetect option is deprecated, Content-Type middleware is only meant to be used to enable the content-type detection, please remove any usage of this option. + type: boolean + type: object + digestAuth: + description: |- + DigestAuth holds the digest auth middleware configuration. + This middleware restricts access to your services to known users. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/digestauth/ + properties: + headerField: + description: |- + HeaderField defines a header field to store the authenticated user. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/digestauth/#headerfield + type: string + realm: + description: |- + Realm allows the protected resources on a server to be partitioned into a set of protection spaces, each with its own authentication scheme. + Default: traefik. + type: string + removeHeader: + description: RemoveHeader defines whether to remove the authorization + header before forwarding the request to the backend. + type: boolean + secret: + description: Secret is the name of the referenced Kubernetes Secret + containing user credentials. + type: string + type: object + encodedCharacters: + description: EncodedCharacters configures which encoded characters + are allowed in the request path. + properties: + allowEncodedBackSlash: + description: AllowEncodedBackSlash defines whether requests with + encoded back slash characters in the path are allowed. + type: boolean + allowEncodedHash: + description: AllowEncodedHash defines whether requests with encoded + hash characters in the path are allowed. + type: boolean + allowEncodedNullCharacter: + description: AllowEncodedNullCharacter defines whether requests + with encoded null characters in the path are allowed. + type: boolean + allowEncodedPercent: + description: AllowEncodedPercent defines whether requests with + encoded percent characters in the path are allowed. + type: boolean + allowEncodedQuestionMark: + description: AllowEncodedQuestionMark defines whether requests + with encoded question mark characters in the path are allowed. + type: boolean + allowEncodedSemicolon: + description: AllowEncodedSemicolon defines whether requests with + encoded semicolon characters in the path are allowed. + type: boolean + allowEncodedSlash: + description: AllowEncodedSlash defines whether requests with encoded + slash characters in the path are allowed. + type: boolean + type: object + errors: + description: |- + ErrorPage holds the custom error middleware configuration. + This middleware returns a custom page in lieu of the default, according to configured ranges of HTTP Status codes. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/errorpages/ + properties: + errorRequestHeaders: + description: |- + ErrorRequestHeaders defines the list of request headers forwarded to the error page service. + When nil (not set), all original request headers are forwarded. + Set to an empty list to forward no headers, or list specific headers to forward only those. + items: + type: string + type: array + query: + description: |- + Query defines the URL for the error page (hosted by service). + The {status} variable can be used in order to insert the status code in the URL. + The {originalStatus} variable can be used in order to insert the upstream status code in the URL. + The {url} variable can be used in order to insert the escaped request URL. + type: string + service: + description: |- + Service defines the reference to a Kubernetes Service that will serve the error page. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/errorpages/#service + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in + the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how long + the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window before + marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/service/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie can + be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - leasttime + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + status: + description: |- + Status defines which status or range of statuses should result in an error page. + It can be either a status code as a number (500), + as multiple comma-separated numbers (500,502), + as ranges by separating two codes with a dash (500-599), + or a combination of the two (404,418,500-599). + items: + pattern: ^([1-5][0-9]{2}[,-]?)+$ + type: string + type: array + statusRewrites: + additionalProperties: + type: integer + description: |- + StatusRewrites defines a mapping of status codes that should be returned instead of the original error status codes. + For example: "418": 404 or "410-418": 404 + type: object + type: object + forwardAuth: + description: |- + ForwardAuth holds the forward auth middleware configuration. + This middleware delegates the request authentication to a Service. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/forwardauth/ + properties: + addAuthCookiesToResponse: + description: AddAuthCookiesToResponse defines the list of cookies + to copy from the authentication server response to the response. + items: + type: string + type: array + address: + description: Address defines the authentication server address. + type: string + authRequestHeaders: + description: |- + AuthRequestHeaders defines the list of the headers to copy from the request to the authentication server. + If not set or empty then all request headers are passed. + items: + type: string + type: array + authResponseHeaders: + description: AuthResponseHeaders defines the list of headers to + copy from the authentication server response and set on forwarded + request, replacing any existing conflicting headers. + items: + type: string + type: array + authResponseHeadersRegex: + description: |- + AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/forwardauth/#authresponseheadersregex + type: string + authSigninURL: + description: AuthSigninURL specifies the URL to redirect to when + the authentication server returns 401 Unauthorized. + type: string + forwardBody: + description: ForwardBody defines whether to send the request body + to the authentication server. + type: boolean + headerField: + description: |- + HeaderField defines a header field to store the authenticated user. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/forwardauth/#headerfield + type: string + maxBodySize: + description: MaxBodySize defines the maximum body size in bytes + allowed to be forwarded to the authentication server. + format: int64 + type: integer + maxResponseBodySize: + description: MaxResponseBodySize defines the maximum body size + in bytes allowed in the response from the authentication server. + format: int64 + type: integer + preserveLocationHeader: + description: PreserveLocationHeader defines whether to forward + the Location header to the client as is or prefix it with the + domain name of the authentication server. + type: boolean + preserveRequestMethod: + description: PreserveRequestMethod defines whether to preserve + the original request method while forwarding the request to + the authentication server. + type: boolean + tls: + description: TLS defines the configuration used to secure the + connection to the authentication server. + properties: + caOptional: + description: 'Deprecated: TLS client authentication is a server + side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634).' + type: boolean + caSecret: + description: |- + CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate. + The CA certificate is extracted from key `tls.ca` or `ca.crt`. + type: string + certSecret: + description: |- + CertSecret is the name of the referenced Kubernetes Secret containing the client certificate. + The client certificate is extracted from the keys `tls.crt` and `tls.key`. + type: string + insecureSkipVerify: + description: InsecureSkipVerify defines whether the server + certificates should be validated. + type: boolean + type: object + trustForwardHeader: + description: |- + TrustForwardHeader defines whether to trust (ie: forward) all X-Forwarded-* headers. + + Deprecated: Use forwardedHeaders.trustedIPs at the EntryPoint level instead, and set trustForwardHeader to true on this middleware. + type: boolean + type: object + grpcWeb: + description: |- + GrpcWeb holds the gRPC web middleware configuration. + This middleware converts a gRPC web request to an HTTP/2 gRPC request. + properties: + allowOrigins: + description: |- + AllowOrigins is a list of allowable origins. + Can also be a wildcard origin "*". + items: + type: string + type: array + type: object + headers: + description: |- + Headers holds the headers middleware configuration. + This middleware manages the requests and responses headers. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/headers/#customrequestheaders + properties: + accessControlAllowCredentials: + description: AccessControlAllowCredentials defines whether the + request can include user credentials. + type: boolean + accessControlAllowHeaders: + description: AccessControlAllowHeaders defines the Access-Control-Request-Headers + values sent in preflight response. + items: + type: string + type: array + accessControlAllowMethods: + description: AccessControlAllowMethods defines the Access-Control-Request-Method + values sent in preflight response. + items: + type: string + type: array + accessControlAllowOriginList: + description: AccessControlAllowOriginList is a list of allowable + origins. Can also be a wildcard origin "*". + items: + type: string + type: array + accessControlAllowOriginListRegex: + description: AccessControlAllowOriginListRegex is a list of allowable + origins written following the Regular Expression syntax (https://golang.org/pkg/regexp/). + items: + type: string + type: array + accessControlExposeHeaders: + description: AccessControlExposeHeaders defines the Access-Control-Expose-Headers + values sent in preflight response. + items: + type: string + type: array + accessControlMaxAge: + description: AccessControlMaxAge defines the time that a preflight + request may be cached. + format: int64 + type: integer + addVaryHeader: + description: AddVaryHeader defines whether the Vary header is + automatically added/updated when the AccessControlAllowOriginList + is set. + type: boolean + allowedHosts: + description: AllowedHosts defines the fully qualified list of + allowed domain names. + items: + type: string + type: array + browserXssFilter: + description: BrowserXSSFilter defines whether to add the X-XSS-Protection + header with the value 1; mode=block. + type: boolean + contentSecurityPolicy: + description: ContentSecurityPolicy defines the Content-Security-Policy + header value. + type: string + contentSecurityPolicyReportOnly: + description: ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only + header value. + type: string + contentTypeNosniff: + description: ContentTypeNosniff defines whether to add the X-Content-Type-Options + header with the nosniff value. + type: boolean + customBrowserXSSValue: + description: |- + CustomBrowserXSSValue defines the X-XSS-Protection header value. + This overrides the BrowserXssFilter option. + type: string + customFrameOptionsValue: + description: |- + CustomFrameOptionsValue defines the X-Frame-Options header value. + This overrides the FrameDeny option. + type: string + customRequestHeaders: + additionalProperties: + type: string + description: CustomRequestHeaders defines the header names and + values to apply to the request. + type: object + customResponseHeaders: + additionalProperties: + type: string + description: CustomResponseHeaders defines the header names and + values to apply to the response. + type: object + featurePolicy: + description: 'Deprecated: FeaturePolicy option is deprecated, + please use PermissionsPolicy instead.' + type: string + forceSTSHeader: + description: ForceSTSHeader defines whether to add the STS header + even when the connection is HTTP. + type: boolean + frameDeny: + description: FrameDeny defines whether to add the X-Frame-Options + header with the DENY value. + type: boolean + hostsProxyHeaders: + description: HostsProxyHeaders defines the header keys that may + hold a proxied hostname value for the request. + items: + type: string + type: array + isDevelopment: + description: |- + IsDevelopment defines whether to mitigate the unwanted effects of the AllowedHosts, SSL, and STS options when developing. + Usually testing takes place using HTTP, not HTTPS, and on localhost, not your production domain. + If you would like your development environment to mimic production with complete Host blocking, SSL redirects, + and STS headers, leave this as false. + type: boolean + permissionsPolicy: + description: |- + PermissionsPolicy defines the Permissions-Policy header value. + This allows sites to control browser features. + type: string + publicKey: + description: PublicKey is the public key that implements HPKP + to prevent MITM attacks with forged certificates. + type: string + referrerPolicy: + description: |- + ReferrerPolicy defines the Referrer-Policy header value. + This allows sites to control whether browsers forward the Referer header to other sites. + type: string + sslForceHost: + description: 'Deprecated: SSLForceHost option is deprecated, please + use RedirectRegex instead.' + type: boolean + sslHost: + description: 'Deprecated: SSLHost option is deprecated, please + use RedirectRegex instead.' + type: string + sslProxyHeaders: + additionalProperties: + type: string + description: |- + SSLProxyHeaders defines the header keys with associated values that would indicate a valid HTTPS request. + It can be useful when using other proxies (example: "X-Forwarded-Proto": "https"). + type: object + sslRedirect: + description: 'Deprecated: SSLRedirect option is deprecated, please + use EntryPoint redirection or RedirectScheme instead.' + type: boolean + sslTemporaryRedirect: + description: 'Deprecated: SSLTemporaryRedirect option is deprecated, + please use EntryPoint redirection or RedirectScheme instead.' + type: boolean + stsIncludeSubdomains: + description: STSIncludeSubdomains defines whether the includeSubDomains + directive is appended to the Strict-Transport-Security header. + type: boolean + stsPreload: + description: STSPreload defines whether the preload flag is appended + to the Strict-Transport-Security header. + type: boolean + stsSeconds: + description: |- + STSSeconds defines the max-age of the Strict-Transport-Security header. + If set to 0, the header is not set. + format: int64 + minimum: 0 + type: integer + type: object + inFlightReq: + description: |- + InFlightReq holds the in-flight request middleware configuration. + This middleware limits the number of requests being processed and served concurrently. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/inflightreq/ + properties: + amount: + description: |- + Amount defines the maximum amount of allowed simultaneous in-flight request. + The middleware responds with HTTP 429 Too Many Requests if there are already amount requests in progress (based on the same sourceCriterion strategy). + format: int64 + minimum: 0 + type: integer + sourceCriterion: + description: |- + SourceCriterion defines what criterion is used to group requests as originating from a common source. + If several strategies are defined at the same time, an error will be raised. + If none are set, the default is to use the requestHost. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/inflightreq/#sourcecriterion + properties: + ipStrategy: + description: |- + IPStrategy holds the IP strategy configuration used by Traefik to determine the client IP. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/ipallowlist/#ipstrategy + properties: + depth: + description: Depth tells Traefik to use the X-Forwarded-For + header and take the IP located at the depth position + (starting from the right). + minimum: 0 + type: integer + excludedIPs: + description: ExcludedIPs configures Traefik to scan the + X-Forwarded-For header and select the first IP not in + the list. + items: + type: string + type: array + ipv6Subnet: + description: IPv6Subnet configures Traefik to consider + all IPv6 addresses from the defined subnet as originating + from the same IP. Applies to RemoteAddrStrategy and + DepthStrategy. + type: integer + type: object + requestHeaderName: + description: RequestHeaderName defines the name of the header + used to group incoming requests. + type: string + requestHost: + description: RequestHost defines whether to consider the request + Host as the source. + type: boolean + type: object + type: object + ipAllowList: + description: |- + IPAllowList holds the IP allowlist middleware configuration. + This middleware limits allowed requests based on the client IP. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/ipallowlist/ + properties: + ipStrategy: + description: |- + IPStrategy holds the IP strategy configuration used by Traefik to determine the client IP. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/ipallowlist/#ipstrategy + properties: + depth: + description: Depth tells Traefik to use the X-Forwarded-For + header and take the IP located at the depth position (starting + from the right). + minimum: 0 + type: integer + excludedIPs: + description: ExcludedIPs configures Traefik to scan the X-Forwarded-For + header and select the first IP not in the list. + items: + type: string + type: array + ipv6Subnet: + description: IPv6Subnet configures Traefik to consider all + IPv6 addresses from the defined subnet as originating from + the same IP. Applies to RemoteAddrStrategy and DepthStrategy. + type: integer + type: object + rejectStatusCode: + description: |- + RejectStatusCode defines the HTTP status code used for refused requests. + If not set, the default is 403 (Forbidden). + type: integer + sourceRange: + description: SourceRange defines the set of allowed IPs (or ranges + of allowed IPs by using CIDR notation). + items: + type: string + type: array + type: object + ipWhiteList: + description: 'Deprecated: please use IPAllowList instead.' + properties: + ipStrategy: + description: |- + IPStrategy holds the IP strategy configuration used by Traefik to determine the client IP. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/ipallowlist/#ipstrategy + properties: + depth: + description: Depth tells Traefik to use the X-Forwarded-For + header and take the IP located at the depth position (starting + from the right). + minimum: 0 + type: integer + excludedIPs: + description: ExcludedIPs configures Traefik to scan the X-Forwarded-For + header and select the first IP not in the list. + items: + type: string + type: array + ipv6Subnet: + description: IPv6Subnet configures Traefik to consider all + IPv6 addresses from the defined subnet as originating from + the same IP. Applies to RemoteAddrStrategy and DepthStrategy. + type: integer + type: object + sourceRange: + description: SourceRange defines the set of allowed IPs (or ranges + of allowed IPs by using CIDR notation). Required. + items: + type: string + type: array + type: object + passTLSClientCert: + description: |- + PassTLSClientCert holds the pass TLS client cert middleware configuration. + This middleware adds the selected data from the passed client TLS certificate to a header. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/passtlsclientcert/ + properties: + info: + description: Info selects the specific client certificate details + you want to add to the X-Forwarded-Tls-Client-Cert-Info header. + properties: + issuer: + description: Issuer defines the client certificate issuer + details to add to the X-Forwarded-Tls-Client-Cert-Info header. + properties: + commonName: + description: CommonName defines whether to add the organizationalUnit + information into the issuer. + type: boolean + country: + description: Country defines whether to add the country + information into the issuer. + type: boolean + domainComponent: + description: DomainComponent defines whether to add the + domainComponent information into the issuer. + type: boolean + locality: + description: Locality defines whether to add the locality + information into the issuer. + type: boolean + organization: + description: Organization defines whether to add the organization + information into the issuer. + type: boolean + province: + description: Province defines whether to add the province + information into the issuer. + type: boolean + serialNumber: + description: SerialNumber defines whether to add the serialNumber + information into the issuer. + type: boolean + type: object + notAfter: + description: NotAfter defines whether to add the Not After + information from the Validity part. + type: boolean + notBefore: + description: NotBefore defines whether to add the Not Before + information from the Validity part. + type: boolean + sans: + description: Sans defines whether to add the Subject Alternative + Name information from the Subject Alternative Name part. + type: boolean + serialNumber: + description: SerialNumber defines whether to add the client + serialNumber information. + type: boolean + subject: + description: Subject defines the client certificate subject + details to add to the X-Forwarded-Tls-Client-Cert-Info header. + properties: + commonName: + description: CommonName defines whether to add the organizationalUnit + information into the subject. + type: boolean + country: + description: Country defines whether to add the country + information into the subject. + type: boolean + domainComponent: + description: DomainComponent defines whether to add the + domainComponent information into the subject. + type: boolean + locality: + description: Locality defines whether to add the locality + information into the subject. + type: boolean + organization: + description: Organization defines whether to add the organization + information into the subject. + type: boolean + organizationalUnit: + description: OrganizationalUnit defines whether to add + the organizationalUnit information into the subject. + type: boolean + province: + description: Province defines whether to add the province + information into the subject. + type: boolean + serialNumber: + description: SerialNumber defines whether to add the serialNumber + information into the subject. + type: boolean + type: object + type: object + pem: + description: PEM sets the X-Forwarded-Tls-Client-Cert header with + the certificate. + type: boolean + type: object + plugin: + additionalProperties: + x-kubernetes-preserve-unknown-fields: true + description: |- + Plugin defines the middleware plugin configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/overview/#community-middlewares + type: object + rateLimit: + description: |- + RateLimit holds the rate limit configuration. + This middleware ensures that services will receive a fair amount of requests, and allows one to define what fair is. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/ratelimit/ + properties: + average: + description: |- + Average is the maximum rate, by default in requests/s, allowed for the given source. + It defaults to 0, which means no rate limiting. + The rate is actually defined by dividing Average by Period. So for a rate below 1req/s, + one needs to define a Period larger than a second. + format: int64 + minimum: 0 + type: integer + burst: + description: |- + Burst is the maximum number of requests allowed to arrive in the same arbitrarily small period of time. + It defaults to 1. + format: int64 + minimum: 0 + type: integer + period: + anyOf: + - type: integer + - type: string + description: |- + Period, in combination with Average, defines the actual maximum rate, such as: + r = Average / Period. It defaults to a second. + x-kubernetes-int-or-string: true + redis: + description: Redis hold the configs of Redis as bucket in rate + limiter. + properties: + db: + description: DB defines the Redis database that will be selected + after connecting to the server. + type: integer + dialTimeout: + anyOf: + - type: integer + - type: string + description: |- + DialTimeout sets the timeout for establishing new connections. + Default value is 5 seconds. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + endpoints: + description: |- + Endpoints contains either a single address or a seed list of host:port addresses. + Default value is ["localhost:6379"]. + items: + type: string + type: array + maxActiveConns: + description: |- + MaxActiveConns defines the maximum number of connections allocated by the pool at a given time. + Default value is 0, meaning there is no limit. + type: integer + minIdleConns: + description: |- + MinIdleConns defines the minimum number of idle connections. + Default value is 0, and idle connections are not closed by default. + type: integer + poolSize: + description: |- + PoolSize defines the initial number of socket connections. + If the pool runs out of available connections, additional ones will be created beyond PoolSize. + This can be limited using MaxActiveConns. + // Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS. + type: integer + readTimeout: + anyOf: + - type: integer + - type: string + description: |- + ReadTimeout defines the timeout for socket read operations. + Default value is 3 seconds. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + secret: + description: Secret defines the name of the referenced Kubernetes + Secret containing Redis credentials. + type: string + tls: + description: |- + TLS defines TLS-specific configurations, including the CA, certificate, and key, + which can be provided as a file path or file content. + properties: + caSecret: + description: |- + CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate. + The CA certificate is extracted from key `tls.ca` or `ca.crt`. + type: string + certSecret: + description: |- + CertSecret is the name of the referenced Kubernetes Secret containing the client certificate. + The client certificate is extracted from the keys `tls.crt` and `tls.key`. + type: string + insecureSkipVerify: + description: InsecureSkipVerify defines whether the server + certificates should be validated. + type: boolean + type: object + writeTimeout: + anyOf: + - type: integer + - type: string + description: |- + WriteTimeout defines the timeout for socket write operations. + Default value is 3 seconds. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + type: object + sourceCriterion: + description: |- + SourceCriterion defines what criterion is used to group requests as originating from a common source. + If several strategies are defined at the same time, an error will be raised. + If none are set, the default is to use the request's remote address field (as an ipStrategy). + properties: + ipStrategy: + description: |- + IPStrategy holds the IP strategy configuration used by Traefik to determine the client IP. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/ipallowlist/#ipstrategy + properties: + depth: + description: Depth tells Traefik to use the X-Forwarded-For + header and take the IP located at the depth position + (starting from the right). + minimum: 0 + type: integer + excludedIPs: + description: ExcludedIPs configures Traefik to scan the + X-Forwarded-For header and select the first IP not in + the list. + items: + type: string + type: array + ipv6Subnet: + description: IPv6Subnet configures Traefik to consider + all IPv6 addresses from the defined subnet as originating + from the same IP. Applies to RemoteAddrStrategy and + DepthStrategy. + type: integer + type: object + requestHeaderName: + description: RequestHeaderName defines the name of the header + used to group incoming requests. + type: string + requestHost: + description: RequestHost defines whether to consider the request + Host as the source. + type: boolean + type: object + type: object + redirectRegex: + description: |- + RedirectRegex holds the redirect regex middleware configuration. + This middleware redirects a request using regex matching and replacement. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/redirectregex/#regex + properties: + permanent: + description: Permanent defines whether the redirection is permanent + (308). + type: boolean + regex: + description: Regex defines the regex used to match and capture + elements from the request URL. + type: string + replacement: + description: Replacement defines how to modify the URL to have + the new target URL. + type: string + type: object + redirectScheme: + description: |- + RedirectScheme holds the redirect scheme middleware configuration. + This middleware redirects requests from a scheme/port to another. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/redirectscheme/ + properties: + permanent: + description: |- + Permanent defines whether the redirection is permanent. + For HTTP GET requests a 301 is returned, otherwise a 308 is returned. + type: boolean + port: + description: Port defines the port of the new URL. + type: string + scheme: + description: Scheme defines the scheme of the new URL. + type: string + type: object + replacePath: + description: |- + ReplacePath holds the replace path middleware configuration. + This middleware replaces the path of the request URL and store the original path in an X-Replaced-Path header. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/replacepath/ + properties: + path: + description: Path defines the path to use as replacement in the + request URL. + type: string + type: object + replacePathRegex: + description: |- + ReplacePathRegex holds the replace path regex middleware configuration. + This middleware replaces the path of a URL using regex matching and replacement. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/replacepathregex/ + properties: + regex: + description: Regex defines the regular expression used to match + and capture the path from the request URL. + type: string + replacement: + description: Replacement defines the replacement path format, + which can include captured variables. + type: string + type: object + retry: + description: |- + Retry holds the retry middleware configuration. + This middleware reissues requests a given number of times to a backend server if that server does not reply. + As soon as the server answers, the middleware stops retrying, regardless of the response status. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/middlewares/retry/ + properties: + attempts: + description: Attempts defines how many times the request should + be retried. + minimum: 0 + type: integer + disableRetryOnNetworkError: + description: DisableRetryOnNetworkError defines whether to disable + the retry if an error occurs when transmitting the request to + the server. + type: boolean + initialInterval: + anyOf: + - type: integer + - type: string + description: |- + InitialInterval defines the first wait time in the exponential backoff series. + The maximum interval is calculated as twice the initialInterval. + If unspecified, requests will be retried immediately. + The value of initialInterval should be provided in seconds or as a valid duration format, + see https://pkg.go.dev/time#ParseDuration. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + maxRequestBodyBytes: + description: |- + MaxRequestBodyBytes defines the maximum size for the request body. + Default is `-1`, which means no limit. + format: int64 + minimum: -1 + type: integer + retryNonIdempotentMethod: + description: RetryNonIdempotentMethod activates the retry for + non-idempotent methods (POST, LOCK, PATCH) + type: boolean + status: + description: Status defines the range of HTTP status codes to + retry on. + items: + pattern: ^([1-5][0-9]{2}[,-]?)+$ + type: string + type: array + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines how much time the middleware is allowed to retry the request. + The value of timeout should be provided in seconds or as a valid duration format, + see https://pkg.go.dev/time#ParseDuration. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + type: object + stripPrefix: + description: |- + StripPrefix holds the strip prefix middleware configuration. + This middleware removes the specified prefixes from the URL path. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/stripprefix/ + properties: + forceSlash: + description: |- + Deprecated: ForceSlash option is deprecated, please remove any usage of this option. + ForceSlash ensures that the resulting stripped path is not the empty string, by replacing it with / when necessary. + Default: true. + type: boolean + prefixes: + description: Prefixes defines the prefixes to strip from the request + URL. + items: + type: string + type: array + type: object + stripPrefixRegex: + description: |- + StripPrefixRegex holds the strip prefix regex middleware configuration. + This middleware removes the matching prefixes from the URL path. + More info: https://doc.traefik.io/traefik/v3.7/middlewares/http/stripprefixregex/ + properties: + regex: + description: Regex defines the regular expression to match the + path prefix from the request URL. + items: + type: string + type: array + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: middlewaretcps.traefik.io +spec: + group: traefik.io + names: + kind: MiddlewareTCP + listKind: MiddlewareTCPList + plural: middlewaretcps + singular: middlewaretcp + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + MiddlewareTCP is the CRD implementation of a Traefik TCP middleware. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/middlewares/overview/ + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: MiddlewareTCPSpec defines the desired state of a MiddlewareTCP. + properties: + inFlightConn: + description: InFlightConn defines the InFlightConn middleware configuration. + properties: + amount: + description: |- + Amount defines the maximum amount of allowed simultaneous connections. + The middleware closes the connection if there are already amount connections opened. + format: int64 + minimum: 0 + type: integer + type: object + ipAllowList: + description: |- + IPAllowList defines the IPAllowList middleware configuration. + This middleware accepts/refuses connections based on the client IP. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/middlewares/ipallowlist/ + properties: + sourceRange: + description: SourceRange defines the allowed IPs (or ranges of + allowed IPs by using CIDR notation). + items: + type: string + type: array + type: object + ipWhiteList: + description: |- + IPWhiteList defines the IPWhiteList middleware configuration. + This middleware accepts/refuses connections based on the client IP. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/middlewares/ipwhitelist/ + + Deprecated: please use IPAllowList instead. + properties: + sourceRange: + description: SourceRange defines the allowed IPs (or ranges of + allowed IPs by using CIDR notation). + items: + type: string + type: array + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: serverstransports.traefik.io +spec: + group: traefik.io + names: + kind: ServersTransport + listKind: ServersTransportList + plural: serverstransports + singular: serverstransport + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ServersTransport is the CRD implementation of a ServersTransport. + If no serversTransport is specified, the default@internal will be used. + The default@internal serversTransport is created from the static configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/serverstransport/ + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ServersTransportSpec defines the desired state of a ServersTransport. + properties: + certificatesSecrets: + description: CertificatesSecrets defines a list of secret storing + client certificates for mTLS. + items: + type: string + type: array + cipherSuites: + description: CipherSuites defines the cipher suites to use when contacting + backend servers. + items: + type: string + type: array + disableHTTP2: + description: DisableHTTP2 disables HTTP/2 for connections with backend + servers. + type: boolean + forwardingTimeouts: + description: ForwardingTimeouts defines the timeouts for requests + forwarded to the backend servers. + properties: + dialTimeout: + anyOf: + - type: integer + - type: string + description: DialTimeout is the amount of time to wait until a + connection to a backend server can be established. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + idleConnTimeout: + anyOf: + - type: integer + - type: string + description: IdleConnTimeout is the maximum period for which an + idle HTTP keep-alive connection will remain open before closing + itself. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + pingTimeout: + anyOf: + - type: integer + - type: string + description: PingTimeout is the timeout after which the HTTP/2 + connection will be closed if a response to ping is not received. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + readIdleTimeout: + anyOf: + - type: integer + - type: string + description: ReadIdleTimeout is the timeout after which a health + check using ping frame will be carried out if no frame is received + on the HTTP/2 connection. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + responseHeaderTimeout: + anyOf: + - type: integer + - type: string + description: ResponseHeaderTimeout is the amount of time to wait + for a server's response headers after fully writing the request + (including its body, if any). + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + type: object + insecureSkipVerify: + description: InsecureSkipVerify disables SSL certificate verification. + type: boolean + maxIdleConnsPerHost: + description: MaxIdleConnsPerHost controls the maximum idle (keep-alive) + to keep per-host. + minimum: -1 + type: integer + maxVersion: + description: MaxVersion defines the maximum TLS version to use when + contacting backend servers. + type: string + minVersion: + description: MinVersion defines the minimum TLS version to use when + contacting backend servers. + type: string + peerCertURI: + description: PeerCertURI defines the peer cert URI used to match against + SAN URI during the peer certificate verification. + type: string + rootCAs: + description: RootCAs defines a list of CA certificate Secrets or ConfigMaps + used to validate server certificates. + items: + description: |- + RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. + If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. + properties: + configMap: + description: |- + ConfigMap defines the name of a ConfigMap that holds a CA certificate. + The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + type: string + secret: + description: |- + Secret defines the name of a Secret that holds a CA certificate. + The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + type: string + type: object + x-kubernetes-validations: + - message: RootCA cannot have both Secret and ConfigMap defined. + rule: '!has(self.secret) || !has(self.configMap)' + type: array + rootCAsSecrets: + description: |- + RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. + items: + type: string + type: array + serverName: + description: ServerName defines the server name used to contact the + server. + type: string + spiffe: + description: Spiffe defines the SPIFFE configuration. + properties: + ids: + description: IDs defines the allowed SPIFFE IDs (takes precedence + over the SPIFFE TrustDomain). + items: + type: string + type: array + trustDomain: + description: TrustDomain defines the allowed SPIFFE trust domain. + type: string + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: serverstransporttcps.traefik.io +spec: + group: traefik.io + names: + kind: ServersTransportTCP + listKind: ServersTransportTCPList + plural: serverstransporttcps + singular: serverstransporttcp + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ServersTransportTCP is the CRD implementation of a TCPServersTransport. + If no tcpServersTransport is specified, a default one named default@internal will be used. + The default@internal tcpServersTransport can be configured in the static configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/tcp/serverstransport/ + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ServersTransportTCPSpec defines the desired state of a ServersTransportTCP. + properties: + dialKeepAlive: + anyOf: + - type: integer + - type: string + description: DialKeepAlive is the interval between keep-alive probes + for an active network connection. If zero, keep-alive probes are + sent with a default value (currently 15 seconds), if supported by + the protocol and operating system. Network protocols or operating + systems that do not support keep-alives ignore this field. If negative, + keep-alive probes are disabled. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + dialTimeout: + anyOf: + - type: integer + - type: string + description: DialTimeout is the amount of time to wait until a connection + to a backend server can be established. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + proxyProtocol: + description: ProxyProtocol holds the PROXY Protocol configuration. + properties: + version: + description: Version defines the PROXY Protocol version to use. + maximum: 2 + minimum: 1 + type: integer + type: object + terminationDelay: + anyOf: + - type: integer + - type: string + description: TerminationDelay defines the delay to wait before fully + terminating the connection, after one connected peer has closed + its writing capability. + pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$ + x-kubernetes-int-or-string: true + tls: + description: TLS defines the TLS configuration + properties: + certificatesSecrets: + description: CertificatesSecrets defines a list of secret storing + client certificates for mTLS. + items: + type: string + type: array + insecureSkipVerify: + description: InsecureSkipVerify disables TLS certificate verification. + type: boolean + peerCertURI: + description: |- + MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host. + PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. + type: string + rootCAs: + description: RootCAs defines a list of CA certificate Secrets + or ConfigMaps used to validate server certificates. + items: + description: |- + RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. + If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. + properties: + configMap: + description: |- + ConfigMap defines the name of a ConfigMap that holds a CA certificate. + The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + type: string + secret: + description: |- + Secret defines the name of a Secret that holds a CA certificate. + The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + type: string + type: object + x-kubernetes-validations: + - message: RootCA cannot have both Secret and ConfigMap defined. + rule: '!has(self.secret) || !has(self.configMap)' + type: array + rootCAsSecrets: + description: |- + RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. + items: + type: string + type: array + serverName: + description: ServerName defines the server name used to contact + the server. + type: string + spiffe: + description: Spiffe defines the SPIFFE configuration. + properties: + ids: + description: IDs defines the allowed SPIFFE IDs (takes precedence + over the SPIFFE TrustDomain). + items: + type: string + type: array + trustDomain: + description: TrustDomain defines the allowed SPIFFE trust + domain. + type: string + type: object + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: tlsoptions.traefik.io +spec: + group: traefik.io + names: + kind: TLSOption + listKind: TLSOptionList + plural: tlsoptions + singular: tlsoption + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + TLSOption is the CRD implementation of a Traefik TLS Option, allowing to configure some parameters of the TLS connection. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/tls/tls-certificates/#certificates-stores#tls-options + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TLSOptionSpec defines the desired state of a TLSOption. + properties: + alpnProtocols: + description: |- + ALPNProtocols defines the list of supported application level protocols for the TLS handshake, in order of preference. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/tls/tls-certificates/#certificates-stores#alpn-protocols + items: + type: string + type: array + cipherSuites: + description: |- + CipherSuites defines the list of supported cipher suites for TLS versions up to TLS 1.2. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/tls/tls-certificates/#certificates-stores#cipher-suites + items: + type: string + type: array + clientAuth: + description: ClientAuth defines the server's policy for TLS Client + Authentication. + properties: + clientAuthType: + description: ClientAuthType defines the client authentication + type to apply. + enum: + - NoClientCert + - RequestClientCert + - RequireAnyClientCert + - VerifyClientCertIfGiven + - RequireAndVerifyClientCert + type: string + secretNames: + description: SecretNames defines the names of the referenced Kubernetes + Secret storing certificate details. + items: + type: string + type: array + type: object + curvePreferences: + description: |- + CurvePreferences defines the preferred elliptic curves. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/tls/tls-certificates/#certificates-stores#curve-preferences + items: + type: string + type: array + disableSessionTickets: + description: DisableSessionTickets disables TLS session resumption + via session tickets. + type: boolean + maxVersion: + description: |- + MaxVersion defines the maximum TLS version that Traefik will accept. + Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13. + Default: None. + type: string + minVersion: + description: |- + MinVersion defines the minimum TLS version that Traefik will accept. + Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13. + Default: VersionTLS10. + type: string + preferServerCipherSuites: + description: |- + PreferServerCipherSuites defines whether the server chooses a cipher suite among his own instead of among the client's. + It is enabled automatically when minVersion or maxVersion is set. + + Deprecated: https://github.com/golang/go/issues/45430 + type: boolean + sniStrict: + description: SniStrict defines whether Traefik allows connections + from clients connections that do not specify a server_name extension. + type: boolean + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: tlsstores.traefik.io +spec: + group: traefik.io + names: + kind: TLSStore + listKind: TLSStoreList + plural: tlsstores + singular: tlsstore + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + TLSStore is the CRD implementation of a Traefik TLS Store. + For the time being, only the TLSStore named default is supported. + This means that you cannot have two stores that are named default in different Kubernetes namespaces. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/tls/tls-certificates/#certificates-stores#certificates-stores + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TLSStoreSpec defines the desired state of a TLSStore. + properties: + certificates: + description: Certificates is a list of secret names, each secret holding + a key/certificate pair to add to the store. + items: + description: Certificate holds a secret name for the TLSStore resource. + properties: + secretName: + description: SecretName is the name of the referenced Kubernetes + Secret to specify the certificate details. + type: string + required: + - secretName + type: object + type: array + defaultCertificate: + description: DefaultCertificate defines the default certificate configuration. + properties: + secretName: + description: SecretName is the name of the referenced Kubernetes + Secret to specify the certificate details. + type: string + required: + - secretName + type: object + defaultGeneratedCert: + description: DefaultGeneratedCert defines the default generated certificate + configuration. + properties: + domain: + description: Domain is the domain definition for the DefaultCertificate. + properties: + main: + description: Main defines the main domain name. + type: string + sans: + description: SANs defines the subject alternative domain names. + items: + type: string + type: array + type: object + resolver: + description: Resolver is the name of the resolver that will be + used to issue the DefaultCertificate. + type: string + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + helm.sh/resource-policy: keep + controller-gen.kubebuilder.io/version: v0.19.0 + name: traefikservices.traefik.io +spec: + group: traefik.io + names: + kind: TraefikService + listKind: TraefikServiceList + plural: traefikservices + singular: traefikservice + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + TraefikService is the CRD implementation of a Traefik Service. + TraefikService object allows to: + - Apply weight to Services on load-balancing + - Mirror traffic on services + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/kubernetes/crd/http/traefikservice/ + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TraefikServiceSpec defines the desired state of a TraefikService. + properties: + failover: + description: Failover defines the Failover service configuration. + properties: + errors: + description: Errors defines which errors should trigger the use + of the fallback service. + properties: + maxRequestBodyBytes: + description: |- + MaxRequestBodyBytes defines the maximum size allowed for the body of the request. + Default value is -1, which means unlimited size. + format: int64 + type: integer + status: + description: Status defines the list of status code ranges + for which the fallback service should be used. + items: + type: string + type: array + type: object + fallback: + description: Fallback defines the fallback service to use when + the main service returns an error. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in + the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how long + the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window before + marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/service/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie can + be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - leasttime + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + service: + description: Service defines the main service to use. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in + the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how long + the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window before + marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/service/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie can + be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - leasttime + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + required: + - errors + - fallback + - service + type: object + highestRandomWeight: + description: HighestRandomWeight defines the highest random weight + service configuration. + properties: + services: + description: Services defines the list of Kubernetes Service and/or + TraefikService to load-balance, with weight. + items: + description: Service defines an upstream HTTP service to proxy + traffic to. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how + long the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window + before marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/service/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie + can be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - leasttime + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + type: array + type: object + mirroring: + description: Mirroring defines the Mirroring service configuration. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent to + the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in the + Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the health + check endpoint. + type: string + port: + description: Port defines the server URL port for the health + check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for the + health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + maxBodySize: + description: |- + MaxBodySize defines the maximum size allowed for the body of the request. + If the body is larger, the request is not mirrored. + Default value is -1, which means unlimited size. + format: int64 + type: integer + middlewares: + description: Middlewares defines the list of references to Middleware + resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array + mirrorBody: + description: |- + MirrorBody defines whether the body of the request should be mirrored. + Default value is true. + type: boolean + mirrors: + description: Mirrors defines the list of mirrors where Traefik + will duplicate the traffic. + items: + description: MirrorService holds the mirror configuration. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how + long the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window + before marking the server as unhealthy. + type: integer + type: object + percent: + description: |- + Percent defines the part of the traffic to mirror. + Supported values: 0 to 100. + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/service/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie + can be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - leasttime + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + type: array + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server to be + marked as unhealthy. It also defines for how long the server + will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window before + marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards the + response from the upstream Kubernetes Service to the client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/service/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie can be + accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie can only + be transmitted over an encrypted connection (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - leasttime + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + weighted: + description: Weighted defines the Weighted Round Robin configuration. + properties: + services: + description: Services defines the list of Kubernetes Service and/or + TraefikService to load-balance, with weight. + items: + description: Service defines an upstream HTTP service to proxy + traffic to. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + 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. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how + long the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window + before marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/http/load-balancing/service/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie + can be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + strategy: + description: |- + Strategy defines the load balancing strategy between the servers. + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - leasttime + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + type: array + sticky: + description: |- + Sticky defines whether sticky sessions are enabled. + More info: https://doc.traefik.io/traefik/v3.7/reference/routing-configuration/kubernetes/crd/http/traefikservice/#stickiness-and-load-balancing + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie can be + accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + - None + - Lax + - Strict + type: string + secure: + description: Secure defines whether the cookie can only + be transmitted over an encrypted connection (i.e. HTTPS). + type: boolean + type: object + type: object + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true diff --git a/charts/gateway/Chart.lock b/charts/gateway/Chart.lock new file mode 100644 index 0000000..6fd9194 --- /dev/null +++ b/charts/gateway/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: traefik + repository: https://traefik.github.io/charts + version: 42.1.3 +digest: sha256:5a99002a73ea2b6236b639a586ca9111b30cf4f57e2e3e2506ffb5468d264e50 +generated: "2026-10-28T13:10:48.091894-04:00" diff --git a/charts/gateway/Chart.yaml b/charts/gateway/Chart.yaml new file mode 100644 index 0000000..8282ca7 --- /dev/null +++ b/charts/gateway/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: gateway +description: Traefik serving the Kubernetes Gateway API, with a shared HTTP Gateway every namespace can attach routes to. Needs the gateway-crds chart. +type: application +version: 0.1.0 +appVersion: v3.7.16 +kubeVersion: ">=1.31.0-0" +dependencies: + - name: traefik + version: 42.1.3 + repository: https://traefik.github.io/charts diff --git a/charts/gateway/charts/traefik-42.1.3.tgz b/charts/gateway/charts/traefik-42.1.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..717c53c5aa3933e150bebb03889a5056557a5f9f GIT binary patch literal 271167 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%0A;>>1T^>na(U-6?_C$;1buFvyJa&jCz zZB!KsJ~NzE_+hgIQ1Gb>xhCqG7i0m2aDwmJs^rvDCIx+6v#@Dv0iY|;pE&$>Xo$0>)iqPV zR|9Ws+iB%5p_3U)uFXAT0XiO@#p}4hgBM{p-&WQ~uzzAzj!6lw%I5w6KisM_U`kH> zi??LUkY|i$dS)8vQeoK=YrLk{wl>ma!jr0F$rF8I_QT+op=sdFdqZW;^Jxuc zB+u2B5mXGhu0StV=j}EPPG!kq8{2dI!zFora_AYS`}rBu!t9DW24+D`4+dISeAMds zDz7Tn0=!1G{c!0VwyxQdoSk;995_XfP+;0n z1%e)-e`3H%(-fxfK^Te@V}am^F-guYE{_p(Z`hF{V?;IbstiWPfD)Sfrt#*Cu#;BKhSqpw%LjH{6+Q!iq&{^pHzbQwu(HA`yB)*)2JX=NS$p?}sp%8_JyElhTZevg;Lv6l z2dsJv{U%kp14Y;^xQtmc+!zJ=4c5C!&b^(-_M!mIz?UR-L6*0mbo&A;O+SBr6aZ(3 z20(m{At=jd?#PyF>aBgZ**#U4G|*P)Ixu)<3xJ=U2BkRR(OJ667PqymjErGxG-KS; zgOj1+6{+f;hJliYtYfV29BExO*(TWq;uH4dnNN{r71MasZ|g*3{-Qhk@HvhgsctFr zS{JBVi?lX{sWsJ9?-Qi%7o%E*s8EE0P+)6DYsFskK61W;o3mQ&I}>)+zBMoI-I;ZL ztZdDFmGpvvPG&qyy@zDY%R+U{dZ99rk}~IiFKv#4YM4NI?F}4>=~A*h#Exxp20PQ{ z!HUHG;o`h9Dg|&qQ?9&0Go?EIv?+Q546&|KHW^yIF0vKv!0e7$0j-&hXK7sPtaUAj z6?&LdSIu;30TgxXe=`mek>8paV)EH%@5G8LZ;&h~{_L~H2q9=qWy8bkbJu2Yi8fU* ze|iby^4%FM?%KK3`oVR2*VO-*A1Q=BISw=1SyTyv^j2O5x;2-`n5QUMIxh-fp=Ltq z_>L7j?DuPL=k)e0&5ZDzt09wbgt|ZZ;k{c9Nfr-l=w{$si*NUDtFI(^ z54h<3%`l*1WP_Bi&MyxRN8Q@rN2C2Zlyjc@nC8Z&;xKk+9y<)Zy>l4HdRoOhOG&~D z!^EqSJJvk_!O#=PVh|tJ$H7*Y(5M~ZK3tr6Rv3f|FH&3{zcX)@OZifj+J2@sJR${S z@`OqK;o@v%o;HV9YxYHtG20|33?_9ITtiH0I?lKPdq~40X+3@(^;P42%BjD(eilF_ zqmaxK(^4wtQi$FS=n9;!Ii@N7Ss`wcr@JBjas^aw$VNvzwExXGH!1}Yoc_X>5;Kb9hC5*L% zu5~V;;FAe!0?yM_a{HL+Tn^8^{q{8`&9$kpn=^oSr|`MpH6{}xMD{k?-GvmiYVOiK;2#%95My`f5=mk@@|bofeH77)eq(O~(em>zQwY ztmeUheH<;dZQ7DouPX-l+8%xH0)dkXROrH8j6{pjBX!65B-suE;%5!-`uA+_JYr zu(ByN*1}nOL>{tiVAp{2TJz9Wwo~nhtnD!%Om=d5=Hfm=8Ov8Qs3!(Pnvn(oR1RLN z>sEDb%{1W%E&aB2TPK059iuU(x-IVQ@p|<_1w+6pLA3UV%KsKj{f0yH)=__bHqv zp2HsD2+a|P&SR-D?sq4{s{aocXKqfH1rnNIw`8v;2r-QB1r;38* zt9ob$n;X08d`@rQ7c2*r_yfxO-2=^v*50PoLui7_st_Wp0p<1d4=ZvyY+k-*%lR}X zj3zU5)&sW3hJ92*D&7V@z-sMvfR%!wlvC(G2l*adcqAlI=@L~BpTcPxV3@REhLUvM zwQ$Vz4Kq9HAw~s^+AvAnL4^Ke^&l zsxiVUpYD|DnPh4vvbJ#ZbqDMqoS8fAhL49}Tx!Eyf~g5NC%Ss(Le#A;ln_NsaUs<(VO}+tqQ+xYGL@VJSIw2i{y?JRo0~Yrt={;pXi6AxGyRb0n>Q}JC8`60)JtTUxB=a>9WfRT%U-b;)rfD;`xG%$jU*p^WFmeI~JI?Znq)N*&(WPW- zs~DWs86=t$k#GoK6rj#6#K2`c>JZH3VGlN>WN{}1UW8x5meT2fK&l$N9cJaY{rhNE zj$7KaY~R_dw|0JaY~oq|Hy^B7HE;nHKy)#W15`=kH2CNeh@s|*iZKO|kI^*O5U{FN zErY@L9Wj|ey^y~yDlduk_A{#^L4SCk;qML0<~F|o?gETMRh7_5m18?zEd%q)K=!`+ zBf3UoMvp&y`uSb`xZGlv4B+<0e^VlVx-t6we*fk3=kR~O->?7w;^oWFUq1iC!HdI} zFP|SgKYDrihrapm=ntg-bRew%DV3)35B*1v)tub#Q@(eo;efSZU1q^`vrF&XelYP{tCU>gH}$u#*`Yx)c98sv}ne@C_9 zn>*TEl#i`%;R`FLujv3jF$}@#a+;r}cTM{urzOqam%1qR1JMICLIQfgqA-wP=>66C z(3jzUQmnB2%f`@N`P?7Dk$zH$uu{@`fbKlO{;PXVaxgtOac@Ps{!FY}3v)J{urL=X zvlZ(M_?U$34onLNH+@|*R^0cwn>li?_kGRsa(?G|@Lz5)Hi9d~M&C)6(nPl(Bloj< zoZL?fPWYa!w&MPs+6YPf*Bc^>|9%^!2^!Uq=i@mfy2-?JYGf%a59Vv`-(O@DsVHmG z&j<;qA)DA|g!!xZKd>0U2Kz@EsJY>`ldVzIx7rw0eTxlI)_=dn9LT09?0(jxvirFM zrTvIVFgC`f27=!Y!ku5Ne|y(y+zjL<;zRmmr<<~PuLUN9S$DVjzBZlqU10ycWO6yA zlFpg_4?;}-#8_d7he2~N;&RgHCK5ggNvO5n_t4!f5`#$oftNw^A9QR2GPsBOSd46g| zng?-?L3?nqFgO-6)u?f+u)WwG{W{~%qrJGK8tqiisj4_$ciAW!C-eZlf8ma#cdG?9mt09=0E!6b(ojv3h6L4g?B2rYmc z^UjZ=gi~20zc$Cb>5ljY8}5u^dD7a%uJGnM z4&Ty|o$UrB+o@{Y&;1Gw_p{B`U$e*W^~#B!-RyNftvuZ)mCy#uGVMx>3cNS`>(Pn>3>)hAB#6Q}u!)7;|c6Q}v0 zFCNBeI_Jt`ttB6q`T+(UjCxT)R8_E}zO|9PLGwBLhsd{dn{};z2`iNuD>8%K zZ~k6)yz5I*{S_=oVD>wf(-Nw)-|GM#FuBw3PNjq<*jFmjCoGw5<;&&o}8bN zi&l4W6|Q5oeo9l#lg9ET8DH-R4iT0+_e}PCk(F}>)ta}O?;>ai85Yvi5eMhT@5>B5 z1i;CJ=UlH|L915`SFQj))YqiB=8ssj{IY)*pTYk#s~Vuhru2X&xmh+ zzZtmqD_MXyqH8Htv$3FZbN_D}?ueN=T;r#7|MtD!d6BJb4;E>yit>l>ZR$Dy>uv=b zm#e`v>*gD7gK226^5L_)pAmoj^e{>HsoRs|%~9^uS7K&_rVDed71YnJ%8~f3e#934 z0*i33nevWp_BkL8{n>kSbv1lMB+OIKwR6YJH**!L(fd*hmC($bxTmK3wvhM6JK!sS z`#cElo&@-+%HUR_)>KZJhQirG&&Y=Y(%zhko7~nYhrB*}ra7}O;dl7LdP7SF)ouBL zWy|}+eb&hgPZ8~Nf@PoH2+99p_>^oY(qp?jOcES=gNo-*dP6`NUI=2`+~qWAV?<=L zQ)N*=ZTzs@aFrLP@CCT53~o-HLj4N-F-(Xur&;ElGvD&ujrRzW^tgbvHN)EMG&=** z-)y;=mOR;FsquTq*s9QH5#vrV{t@eQ+Ekockc4JQnNjU-H3dlqB(5S!3=d8A%&WQ0 zq*H%I6DgDe=XVDI){#X<_5O3z;Ir>sl4^H1UzVzk!@x}3+<}48eXWICs~YYBV#2Q^ z6*oC#Itq2p2L^|K@5l%9y8@be!6q9ocD)&~nT@REWl6Nx;Xo-jnz1fsC{y+Hqd*8M ze08{0i9uG&!%|4pbi=ZY;HJ?;mlVp9&S*Ym&VWUNZ0j92k4)h+X+kiTrefFMVm!&&t&5kssZV&!NovCJL8OISa+vT|P@6T>mgAT6pzGa-cz#&dX)4>Fwss z7&0O*Kcl%er+7pZ^>;vnsV=#o{RL z_vYMY@#!rNBQ2uEQExYAH__rK!eOFZXv0xPNaF4d1YfIqY$N?pnE2}a@;$7;<~OaC z7cG~UD>`B3_+?|7Tw@Ia4B5UqzqD2pWm9p3imFTIT=OXf^UBBX-dr9>(-$vhk(@q% z8Qp%Fyr}e$Q?6&_7?Mu8b#wo_ij_T@F1QksGu3P9Yd@|sBRfqSMByDA{PQEY=BK4l zn4HN-+)sTNDo{>*we^knInc8Pwc4?!77!u0s)?9PGJ*AG6i-<~H;541f){Kmi5~<% z>WLMh3(7MruMD0+w%RSvxvmxn*p$|ld%{^6sfKQM?t=ML7(;@dg1z}0HN0hHu!CwL z-OdmWslx6ERDY1zo8}7jx7+Fy3z3qKr|&N9y;`08>vGIw&NNeoXr>}j!95BQGKTU7 zpGCCby~RO%)bH&UB29CJT}F$8$Xqi|J>MbZ>wL(3&2%x_XAH@LmV{5pXhp?8dP-Wq ztzN?F#R9_glnREac+2LHo?*yj!g5Wgo2%@qCQxL9&4paLcs~JJ1cU$Iy?^`q?fKc; zvn#B)P8CU6!sj$2d;R!J@-_Lg|4&`gkH36jet+>#gz4n}XYbwC8_AJ0!Mcv8z$mJ| zkg|A?;#66h$=*>YA~N(<9N3Z~v--=dErRYKi5BSa0^pRG*>$(~b3Zpbm-`5_uQM+& z`w08N41l}y=uT3UB2w(HD?_9M4j2pugB^Z5Xpv8em_RD1Cam9Qk?~og9zqD-q+9O2 z9;VdADjve|=gg_N84G1Se(0PKsEC1)1>67@A4|q7c@rKZzlI~RYlZ3)H3OSJ$)^_6ZXcv7}K_6}>>4s^561#9-DYNj%t1*Z#xSVLj6 z{eRYV)aM_E%nZ@b4;EM#4hYqgz>%s`8ZkIKOb9Y!PJOrD7Q?(YDO97#o9 zQ#O*x1zV}Y)=@she8X76+JXpXTS<7+gx_=G0}K(!6KO~!%&YAHwpaQF7UY(M@oWwG?IzE>?;U45B82#RPah;B{HNkKH1EEfgy793>PD&hq;x) zZs1hOkv&N>tNJNwb-V)zl*0<_niw`p@zAsPH->&Y1_g{jLwAVrf?oyIy^ZAypZxl( zZ_UWhd)`cIfBbomw8%gI`DB}{oyvHD#k~T_VB;;8820BGQSoJxDX9AEi-5vyZrd_I z)pbmNEXO>!IF^Eh--og;ShUh%GTHNveD@v6xA;LYGFYw1{#TB;0l$CC52-Z;#zr1;UazNyDqJ5anm+r<^Y|O0+@{VgG)dXxyYb=lyF&SbPc#Xs|9ujL8 z6H7yIa_pb=p+16UrV-p7e+gQ2UPQrV7#$tn8LINO2%IH`kJzc8TGt93wMa0#)^cYJsbU=^BZjYl3rgi-e%8z$G>{M)t~bQ?x%eKxp7GOIeK~=1?|Y~)!xWcs zU5(-dD!YL&;Uh-&p{Ou)ap0AO0zX2a4&+Tkb;Gmo|8n;JJ=A=&YJNXxAhBdv_(Kmf z15qzq)f>zxMIX-2pruBsnY`wUqY)R8_op|m|0aDawt zr=!$)qqdi{H20U4aTVdOqAPNdqhKzcPu*fJ)80!c+Omi4js>F`*U1KXlZ?5N2^^|p zs<`t~7c*guoXLb)BQJy{nYoGRa?d)V!2v^`rWBtJ%BSO-c#YAV2iPg`9tAFPO7F15ni*wApay@{2 zsts1|*cnFtvebk{^b0<^UN*?#@$Hs;>nA|?C>0ROkMT-7L9kmF&(GIs*wGBC3e9=e zs02#A)b*zR1)EttV{C8eCFnuYXVheB%QZ>zL+g8L*frf5OK-*m!OU8skedQ1k>vFJ z^}XN)z)V2_Shr$Ld3Ml|7l7NmPJ2wtx}$^iWFZFwsPTF0teS3thMQ8aih^`w6#WDp ztPY%fV;^=|99RMr?(<|HFNngzHW zL7j&wHQ>IFWn%63Lm3r}3>rN?k6--x#Y_MxSQ-V`{r`PiX46h-6wLb+l06mTND?k>!7Ks0aSr zMc1vKkc0Nq!>2FWM@Q|$mu*LhCQW&BeAIm2e#{dCrMr1D@R27?_msU7PfCCu-Qq;L zzB&U?vU?%~79$QugzlXswyqrsa3aLASALYqGFbBs;{fUgn*juiSBi9NMrZNGj~iZm z(qMD*`*fQJ5ib>ztvrjL$^~dwaq=_E$xpv6aq@t+AN#+{qiEiW?@t@H+&8>ko$#9V z?i!QO<1wubDEG-5HGEHauKx7S{aJ`z&lk3(`9Qa>zSUD$8Yzpyg11~q3r`0A$?;Lk zcU!rv)NO$91$*d8kaTYbxfeXHas2>7}X zhcyb@)WY2(%a4QU&vxo!rORR`XP%yk22jRlTRJ+s_{k5MFgz8E1TjNKbP^(=wAJJ6 zlMSLXppnlWeD9toksPJea0KNcDiaK9RAV!=5RIsbhJ@lbF^dFM2(5%3PmNCG+Gvdn zjoNF`*TIw5$84;;hnGHaJ0*#h3eovKpr`;LVr12$yN(MesqOZ87351JD5hMK2-5Q7 zP!Prrxu6eP-Su)gO4nAvpaunl$F{E`x22@PWpYqeal@%?Q+r!KGOIYGWsw&CaPrss zTMf|Ke4l+^gVGh5b&Az$=euF_R4l5g+UiS-!I*B^V-)mL+EJJuxOa@h>9lF zni<{9ch&!4XitA9c#_>T;D4V!&eE6~)-*#_C}qBz4EM^`+^skeAS(9G%SH~f)wh-P zhO+LELOUQ(eOT08K=mp%;M%A)KwEb)Izv~x*|s;ZN>5fmZLOwcE^$}RJpRD0?i~;8 znh$JS?0N^c5y&(9$gWqxu&iki+*uk4hHiFqXU1%X2`C2goQAb}-?lK3Q% z$z&um{aM$_JQj>dwdzZ6Bgoq=EptiOk3~*MPqTk%@%*my(zoiXqpTt^yST z;k&$-8J-NT98fNB5Cl%g4)zw|ZiRq=U)>Mng7sZGIoxCyIzwj&Un1-r<#NUK$D_{AwLj*j4_#jAztz1& z@kOrBYQwDhqVU`;RsTFW^B3O&01PE#DtJr;k5~ercvoykK~aE@2&IQ#$#`&Z;v4-zM=(51?35GmQK4@u%?BT(%*d3 zn;53Q`3B9c-eePk+Pg=G&%b@~rD9qb++t$eI_6zppsNHs%ru&_%bh)|(A}MuU&7)1$a+I5uL>w}+n8gq z$c*3PKeFCCDX&OaS66KYH)pX$4w;x&Fb6f`??&iWq|D#pe1pHP*yQg3qDwV<`JAvM zlJSx}pq)?_Jw;YKT$`4Qh~>OcgvPOAT9@kV9=72Bj{p4U=g)rLt4)DzdRi5OXy#8z zfv-*h`E4WaLTDVs72ti)O(B4+09NAzs^4Wj(30Fp@Io@m3}OmgBiEO>j%$LJ$+$(% z%o9ywnJ`~oAE5&A_q(mWhu5%5#akAUval#>cpDO|=tYAVFH8p+(j*p4J6vvos)!M~#6GHMherf@PesRVj;&IuHHX;XoNM7_ z0U_K9H+-k%c$pb{GxLjt87ONbX)j!s>P)(>UE~%*?N=$!oip+wC@%LyftD#tPUh4p zS@^cwQEC+HE2f8MiD7Cy$A`7B;q-0Ub%1H6&}S$(X~|#0^KM|U z9&BPaso)W};wOfLBk<~Tqvw>0HI}6hr=17u(t~yBj;%`tlkYm=ZlxQa&q%zcCK@uV@{#vns*>7O zJsP#bk8w9YlA$#jGBF~Gp|d16?puMAcZnBr7h%UW0k70HONhvRU+@|QB?G9roq9k{ z@ji;YDDWoOojW+4Faz#dc%DxCeio2S9Bom#9Q);M?gEv;OPZie{ zMDXoiRU#S<*T2(Stp!t3!D>NlZ-DtH6^U@FfUSK~RMJye zNBbY!r3cQs7xQ~Hc^#l>*EC*vM>QKWHSsx;_G0Ph8_^WVkmp?7`P8*hl+P;(TW8j; z535@MD3;Ylz`g``M1Ks9{K+?-QXdkbU z6-CzJL;tB0au-uWF@SsH>A$9i670)iK(~m)jdLnW+;C8U$eno5#)W3CR@!|elxN}} zDL0qAhF2x@Fm35`9pNQZlcBsOLM8)3$&YZaz_=VCqpJmUbo6rv<$5%$p4t~$R=-rv zG_9WN=Rd1+{`{xTVh)W-X;#Bb8(OX~D>l&Le^sty7q3^q*N39mspRTh{^zup;3){^(7(bt%xk?tn z-gfT~N3$fcZsox4MnXh%YkT4H{RKHa`I5vz#A>8=&$OO1edzEDB7m>yesRUqADH6( z`iUN)uQ}uhCFAuKPstcxupxOW-!!H_>(J+lZI>Xnzu)lj03M&sPY2seD3ffY*K!FZ zDN{}FjX61ek1i(XT$LrjEYO9YRJB*+Gq~&Ip(@wsn^bBRZ*CHM105A>TizGbR|VJ3 zs>xk2=PNGwTs%X?Tt`rz##sS1*>}1}!GIzYFYixGCVm|AkG?Ua!x8KVA^* z$KGGa+3SW2I2~X{FL;nJec;5uu0_rnjj@opudCRRF~pJ{lN6xn$l3YD30kIkq76+V zMwAo`z(rXyzT0TsZFpDTg@kc}E=oC;;LE`1WI~Y|3R@XkKPBrPx@qkedvp3U#F^c(|8>h3$b3Bu z_eTtwgPf}d#;WI!fkXiuVjs1ObcWpZE{6PK=(-)}-4Y9N&o+<|r%&O4bNz(-myTpw z5RCphHfaF>q}2wxzY;7z_wybECr8)?1}gu&*Kh^vj>%8{0rz!C&|{VCp`Ti=fJNoe13sU^Wha|9T)naDn6!$HAtHE1dr??$J6o4 z7I8rCh~vOizT|UuLG-l6Q`=v66UkSx)k+lYnHF_mTpc}XH37=iX<;e6ORQLTXJ1vU zY^L1|YOD}ekLT{{-XMATvPN)+ImvO?_xHgj1}J;*VHYU)?A*LWbcS3XT@Bzp4R^nX z7JyA+<=5D@^lGMUe&+Yk2WBV)j9lQ=>5j8XOFHsKM8m!cdigx1yrBJm`3kwO;{Lso zS=y1xy!J=peT@^Vs~m=gGiQq8W6Y8A#|nV_goO;NSBgbt?hxU6*hGfi7aNZUEs|l?4TFo`$U3G(FmcjbM0zc))b0SK`$CkaxU0_#STWB z-9VD%w}+6!@%;I-wMn}me`C?7qF1RRn^%IVU2iBWhOpkNmnMAiNXaTjx7)VA)Cn5f zv3QCEXNfu6Tt0np#D-HFdnISD*WLuT8KHxtLr}n5nKz!x_j2yF!>U8eW%KsE-peU( zC*Yto-%44lvjh1O;s&;3%L>%GC&sMW$pqWv_S^A!OCdIP3s2uM;p$bK>m64GkwRSZ z5tG>_#aD2O2K%{ZGprLVx@*m#eo*LEfdZ`(u9jEo{B3R3;3MM5e3ceBE}+_K=HM)llhX@?C8Q#LdQ)kav(#x?xuwQ9CHf z68?`2g9F#aeBA8Xy}q-gmJxpK?18njAj!O7p7ZxP4qah|o@kP#f+tttsG6*G(IA^_ zOj18^d=K+e^rfPSu2np))XN-!>w?@f1|V%vLSPY zJ_<;P>-R!7FtHUsYh)0>t0SHcnYzgM#*>(03hxPaNDM3{_$s2|UBDEUamHBo!EPqY z8Aq+B>##%^#T}B;0b9PiyzH!wtySZ-jPNi<@ z=hqfL)pKo&pLLhMkF_v(GI008EeV9R1zhN93xY266?0nq-n+cRsdJ=o7iQYfp5PjA zYbqc()q3w%_&oGhD{^6FZ|i5I&+TrrUF1LO?I!m+;xG`Eo>W&YX=!cbIx7^=1NaCQOOm@KH9JF|0sN z(NOBO{AS^0+gbMQ$fhrC*;f_auGG(h)h>8<$UbWgTBPefScKv?M97peb;^|ae15i7N`5{+3(2dH?H+R7FHSy^&*x{F#F_H? z7#iZ9=*1`5mr7qHr8zzMa=q2KGPNLT=JqoKH3gPes0$fV{VqP9K~%$6#{@d%{r-jo zyq^wAUL&t{X-8XNasS!$R%%qH&1*TLe6zzPbU4lXowK)Z-w=2WiQzpD_t1LX{U-0B zwOic>kk5_M2&)Zim)R_tFudslM(;;T%Pr=A6leRDCV=c{ic6nZFA_V+3{Ch=M{zwm zWrd=o?mSW@EF6cWv@SY;30eFnDrh%8IW6TBZ{bmPpx@}%xXYG9uWR;$3poPri? zI<%Y%1{I|580@%aDU;=^<-7Yz6FKMayF1T$ysbn1o=&#__j^hW8%QCVAnJUr}LNlV-0>xb0V*-jo(TNivDQd_If|F@jlT0!pAF7LHmNATU1H1pEy{~V`5acay@Gl%EP^Do0T2_IW z8rq{(%4yH3ziAuhkh4ZAMtIVfYUH9x0^JU`NVMvv;aek`zpPaN2%F(A*U~2}Bc~NG ztECUWAVwA_`19V+dsT_JQi3d-TjiV5ys~?~v8%|6;DP1DVw`ZG;9X2nv&8U(-MRT| zM6GzyHL%2%RrAmcY+fVE6OzandC4wL%FGFVptSB>}wIoq?oe+x+^TOIf2I2Ed3GC{A*Ru@Qn-iZ zPpP<>+BmIfU%VgUSlK6YfHH>t(2OWnaoTh?k$+{_cQdZb4L6 zKK$k>tJ&1ScZbUAM%QXA6Nad?my>jfG@gY9PlcQiO1eK#!DDKteG-p26dmeF!J|nR ztLQ+96>!(;P&V#e*0VH%q#V-yl+u4>tP25o-K(s}RKg6?EiT(e=q|zW=@vONcI!bw zu2NT^NkGXWOZ=%WoN>BIqey0nX#$RFp;M>NYSJBMy%zoo*$kg9-+c4UG2zCF3hI%e znou7lCW>j9sfaZQ6Ddsuf>k4CLf;4|*Fh)<&6jw0NnPEqBGG6}&HiD;5Y za!jzJV*gk@M5Y*Ug#$1+s2aX z`RuiP9wC+W^vE2rl3MIh6Ez!&5Y}5AVo^ms=<=6^&fl>76UFn}#-A zA`yKWD>&h@RDe#>B9}6_&(SN_msaTVWY8dv{4Hrj>PlM0q*$P{pb6cr)?!D>PGw@) zO%1(wx30XDs_o#{7i}=OsCZ;vb0K{7YhNXqC8o(^|5zl`+KKeL0Y10uw@cE%3oT{t zH%U6Mf;%P)vdulG%!Q$*e!{|Lgwgh$JElveCvOpHcY})pVt-m@iYD*tsE?O)lrq(1 zV;);ZEcOFBq*aZcXyQ*Ks)?5N-fi{mZmjs2sjk(&!@(98^!2&Q7F;5VcLR*q{Em#D zwVocW)M>WlLL9Q_3hsTCgRB-7Pwz2v&Ft+m^oiFd-9R#ZZz)6IEO%r>uSi}}J_udp?0)B3_lMF)8?FK@}2-j~EE##-df4ULSL5SifZCzYDt zbQ`2Qh|(_9W~0}-OPpc9LS?VpjtRfMx6xm{EV|l4;h*9uckzs1uTySxyMA!K-OD2% zhul%D&u=!bvIgCdRIwCA0M?@?ScD(#GjO@pjhsFJE1wF6Mhu<+d*iU~CB-$0#a4=wki6Wv(aj1RGh>z_Ej>}^!6R08F;IVU$%S{ylFE%g1mKcn$sE~+QHs2IlQek|CF-iw$@yx zESZJn_G|7p%fn*h8`fIvYS*KR$Mj|uB`YX)gDCwCsT$v(UOea;zXe@Gr!+|_Tr6f9 zP7&+-(@sv5&*x`BXT|<=^Lv|*^Dx694`kGCDzP$&g#OF8o$)+ zCSq1_3|XP$+;5P-qp+L{{QJ!&Ad>*N;lAAhgMecjMS=123F&AojXpD|Q>C>y`%X@?PNjv#*S6 zqC^gBk}2U4O`r^A{);_dn^H9+X~m}50`5tT-Gc}zcfEJc3>(=cKC0M+yXV(0p}89; z@ecDl7kE-@y`tRpRF~DdDm2>TBRZH3I9RR%tx|dRVm@F?fR*Qja)E~$#zL^Mhkw;W z`t-%iW7>;atycG-MIeG*Gt(f3UNKEl#Ud6ndu^6urbyS&LAOONm?2#UCHra2l;Sb# z5-Mae&^(67Q!VqL(F&Ne~jSP3NLfJfvik=N)32*9-56jUlieuy#b7Q6*8{)Nc2 z6+%8&LI5s{_*~?zr^YY^z_n59j%p~iA-<2G62bA?jb(JjREx_l7V>lrJ4)V6Fi3kajAnm-m-Wa#o=n56 z=fe!)2r?>Tcg7wS$(c&cja-|cc?VO}H2By;TDsrc?!V1p*87U+YUq-n-zkE|1+vjJ zb+Fg&jvrD_yiD>!tARJc6zsD;0H58YOz{y*3>8hQ5(#RUHcbeVO?g6k6DPGA(9%j# zSjQHA+ij5((sjsh_(D{G{wscmPvKWx63J0dLY9Eg(q#>?m<*X>{~2uy)XGoI3ewU( zer>=1OUPDm2*gZd!R3Pdg*@+&9Zv<>2k>H=kXZ4)$t7`?#7v0^z%tqC+#}|=g+siP z3E}cMX3z5(6M_)VYmN1Z`^-d8fYfa~A0^0?+tIk?FtNjOQfvC1um#yDHPeE95zt?0 z0?9xztxpA=Q_6fpWn@*L`3$}jW}8srb|5(`46Z1PwwjE>wUd+cX|P%6OH;d6AGlj) zRwU=S%;rS8PA|r%L`+Cb^{^+YiXndS=}R(XRG1;iJij{79ifWlWw?VqG>?k2_i0?- zYpJe;q_HNa=R{@(ltOUKa7|9FN;oYnHr0v!BzBheVKr0`k@evJS~0-7UJsqKbr|EHgSCa-1TOEQs}DvZb~ERg)4Oh~Gvap6O7wX-Q!k%CwOq*J3Bz7s`~JXJWnV zV_;q7YMrP|Ol9)V@H(D5J8)x-cpM9MO;rWMXw`K9wLnV0Ec97e%rdx0!{;myh4k*pLg0Ag%UX3jSvSJL;bTTzmpUj=J=T|qB&T`ILaA2J2np5DV6jHCx`f7D z??b{3r{XmebaKHWnJg(UxqNCGTZecyHydrJGJYz3uC&1kZFd?eQ(VT|o`8cKD6AO_ zePP7cdSlmF6fv#WV=WijSQ@13kQE~35EL8Os=!n3!G5+dL36zsxgfmlE(ZoGZu zsh#k=ErY4K8}F4zOD0+0^(Ex2|9`eZFC#xeh7@?>SrW0o?GKGf^>Mo$%c$kz2vP+M-5N>yI%Uc9n}0WRbmCf#!HaxXMtPbo&thb$iV>O2W<}; zeA1WaC#P>3;WL%vIl^mn}ls8@>B}BI1zL%$Z`Qh z5SA79E8;g<&TiNvDn=YsRFfn768;N|iOfuzZ5i!U8nHN=YRjhJbq%B!1)A}MNCk?D zbdXr&Q_7M{CfJA>H6b!_s97H+oEy}X0^I=K@x7JJ#T+*`vdZ?rjZCWpl}2AW%;usc z(JYDap!V<&<<&WrLa@k4^Ump=Vrgz%XY?&>;A6y4i-yuiJRZ9Y5vkl4C3I^c zBP#5}$IqV~{pshu`+y_AHVIA;xLPp?u$tn76>Q)GJCGe{C@qjGX=GF#9OjmgR=@6A zi9^f~URwD952QlWF~Hhm{&_D;VsSrlEw)o_af(2*^&wp1iE}#*xpvo&hCS$mW#fuX z-1o%{wR=goTPWfAJEB3Z$w?eYEX;ehE(z&!4dD5|1M zn5i}gRn0(t&@CTy%js4Ry5)mz`Jh|Y_&n&AYdx`^Zt0N-PmEE#mzgyJvtF}4PnagT z|1-xoq@mkTOeZnnY^Zu3ZzWpLg^Yb%r>@SwUc`{qw|KPm++7;SXD$M;c9}LeB#0OYVT7Jf^)!yz7wc=Gar`C^ z;eNkfH1HJ3WP%aZWTsrBJ$k>Y;t@2CA!Jl-k8~JkT$RH@zh4^JoQT{W-F%?%*UWCW zA!^0O1 z(mFh{|ML|7Y5&Lm34gt4kfR29YX58hdHA;-M7)3XeqX<7A3VRmb%P8Vz(`z4N!0_W0ocUB~}h?e;fAw=c#8>{;l+3tVjVpkzKM znH5L(pkzKMnecf~GFOi4Yg97ROb<6N$4-qy+wtcy;I9@OzKagxLe-hW$~Njv09GEn z-c_df;PoziGsmx+Dmh|i$g&Cm%@5w)d*E(+@b0clwg>O-g3mI#VKLkK)Ov;Oz~d>X zfwG>Cbb(yo{k(VBI%*uYo;8kIPa7{~txbk2Hk2+{%&h{7O%A-UolBL=xMn@!;5$u&r)4@ucK@G;Jj@x!yd@-U)Muk1I z?82ZKdAp|b{CZGvZ!8+uR;-RR<*ss2{c&v58oa4P!RZrto2P=Do&)dAIXGq|hkbA) z;UqDXi`tbMx9d&-yHZM{8nGC)7vvTU25v{+`R=L#GQDU>_ky7dRfb|PPv(9x4|VF$ zlfmKP)bS58;YL=iqkB6HToG`1W)PLBe|y~P$#>UG{BT6f=@WOWeN=!v-76Gz>X)zOinDZ8v&#(!t(_}pLC%g?hM zP#MU(jNUzq+70d@Si*Gxx&t$A4_N%12}U&|AAQ%DJHo`MMa~Sl=0ZR<5Zdpv$P^Fe z*P7@o8WO5Wx2$7#u^@4$WL(hQwPq~i#XE0$vc-qNdNX81^HC}o)TqvDS@xwG5!olU z0nyL`Dgnzx8%24tvVi5>vC4@5eZ-(Zbfp`W=S*CZBD6fpW$Yg4}>8DVd zcV{ljVb;6r3l&WGZjabyrM)*Fz%5*fQzdU!2kAU4+-3u~?W2Fr?pG|8m8I30DZ%5? zgvf|12&i2fSG|HdA|pK;<9;`S`qSw}hiF4ntB?BP(KCnEI$-Wb=(e3`d#l17iyp*G zdG)C}FoKU@g2*g8?a&wbeEvR^YqPUqH1@%eGJ!%7V;<*`lf|&rRkK@kbEw28{XUd) z`1^Q)g=(o*a`x(jJ8T!~CTEEq98N5M{FE9h=5NeLw@o#hKmRfRYW0`a7kQ`8p;Del_x6izxPU(dzmi^T{ zHK$`XO3maouMXe-(;L?!FXvFc|5zoq9mxd3jN95sMF*@smLeN5-F^%&jJY}pb)bNc z3TtAS#5rRZg*B`<)%8SRaby@%+wtER@&inZoC?ZEP?H4nPUaIO(@Zr78IM^zbZVNO zGH@A-Xkt$qAGu(u=OVasnW705G8v%jqg(vpoM|Z_9vkyw>}AsB#n5QPT5d7A#L<4M zqLHmn(%p9irlHTN?kn6&)^>@oTPC26KLu#RE2LLz@w; z9T}6n6~Sgp{`NQL~lMP zbWFLhz2EL`@b#|N+b|1w)zu)|J0^$KCd`wQuXVcR?K8z+wabR3P}43`+)Th2c2jYq z`9{RGT5q3SOLZkAji+|n_^AC@yB9Pge8HACn#z(B{k@V|1u(T*mFb}4k1{!za!IkT z^{!{O>(81kQ8(8s56GfOj?#{jeO_U|Y{|CgdaGz7S{Y7S)v8hH^igyC(JL5k!Ook* zdMn9Tg@k($J$Z8a;mxZ_XPJsa=Imgo16YiB&7iK4{dwaubA=l1?Z+@kt33B6>52;a z)!f$$tZsWWwd@OcKij9o(i@sfJ!mG(SZT(Sf&Z@kn5L<4)v%i$j;IO^mWLBpOJ-UT zMm#@+cp-W9>rfl6I&t5406DG;RrCqf5SoVWwfuXRcu7NyS}d7Po1i1nvQ zu`}*maMCKpSjMsRwq-Fc70F#Kq<^dQcJ-W@>c|!O;nOt#tQje_CcJOSwgoz@R&_`P zIxGuNU!wMqCDg(Ug<53#H<^(Uoq##*nqoesOLVZ;CLb?DpM zx8$$Q@Cy4cp9{1sGYy;h!^{d^E4no^4f2RSP4@VGI7R5ugNUV#V_l zO$6AFE#t_9P$5uAL$qU`ModG5PWb+;qg%wE8D82$;$gu#gJ(g)SnLzn?Nj(7$WrxT z8{sXWwQS*J-n~L-mt!s6#5ep6s3j}G)^16{Q^BcrC(MOW)UZJ{#FRxo-J!e&gcGJs zkUZc%No3NbA|28s8!^Qr@J7@AOI{tF|rKXc*X3F+C+OZbA%zkTtimt_Bl ze0X&WcZq5@+wl1IE)%P6y#BLtS$G@^c1_iqYqGP;q3x?M*jrdjw6JY=B{y0d1SUYH{}iDLwveA;^2fXMTRCS*jfAV_sY6P6g_siGkuyTBtY zazRIoXl4M(H>HY>n7vOP6C)c0k=GlAo@f%2RB-^OZfzRwUt-qMPfd11N2#cKKPPh* za$5($M(n6*!VUk0EscaMwYdE&y8Yj#8yIwS3v!{}8NU8uR{t_^WPYLL)nRP2DqZdsOM z26H48+ldz-e_aKu|H~#C|Bnt@wT%nrSqzJ_rVUm2l*V=aUsMk zRSbZ*_7Iu&!R@f}*?aDGSomvAZ@4k(vpaDT#$)?vmH3&^W=YED=hq#Kr7@C(t_j5- z-(6mI+RyS@Ep%n2_m=@Sb1g$71~$y#YF+mBkU$+ii6M;q|d0y z6x*R#^@_l=gBH0QGR5G;xJu-8LIfXim$ZpVjz&SIY&oXFUdkX|8&8PS4wS-r`Vu&D z|ED~3_wn;TJ$?S?eE0)yhFK2)LGp)+|KwU{Ot+!gWB3E{QB3p2w}1Nc%fmlCKRP%# z#t0C|!X?*IUD-2FW=TwVUXe7;sPHI@w-9=?B~g^fnAHyHxZU_^ONXW%mtEm41Ww=E zP!ZX)bEZ8mxLK~H-PUmck*n`zMbxbM5=grM*rt#JokvyX7M)7Wp9h}ail%fAFWkPO z-X@gy5z7x$_n*}vnKWWQFx8WK)usz=mS{k>I{rK(zUj`U-Ci z)u&I8aE9{5;0M#69mF6x*4Su&d-i(R-~O{6G*xXEB1&@pmwu~`vK^dm<0<8r-HZN9C{O2{l;%(=%Y(l;X6+%W14&}B| z9ny8O@t`c9C4p~E_AgI6?a!|}l`;pH+2EFp^JY>lb?~dp$Jj6_A*A;)(y>$|o{T=* zBBe^_GK&=}gWc2Q1Vhz;)emfc&HHfaH3T~d@&7)543L@%31IyU6@#-5v9l!aU+pRh z;ND(=jG~FIEmSX%*x1rn=+tG7Tg7mLJcCaJJ;;MUZL!geX4&|4Ac>RTGL=_8wV8j_L0j@<;8x{c#Y^y5Ja7wqw!J55{6JT-b-G?FZXOsG zK06X`Te>V|so=@-QKCU`PvvN4CqTtIyDV`-+L!^O~w%j#h`H0e2co*M$j?GhTzRj;T5 z(o$NVw_G6l4@>;k!U2(eKt|{>@^2>}-Zv36!mzkO29g*yN(D8{1C+D)fb8qdG0MYI zw86Nk+m?hgtGbGmYgu`#MpG=ToM?yYk;zvYODY@XmJRuunrL<8^Gva0A>?%~^^7Dm zGZLV~sSpzay@Ftc8AU^y4A?TOZ_NqSIxuS4-scIttArXp-q|)>my6sf_ZJ-PZrKP* zCsi)RHJ6=8NJI})I71XFCrv8z&n7j%Ln)^;-_15v*?rK&dyv z)`4FE9b6RJwTOeIL&r^5^=Q-zKgZoh0HDL2He_N%lq(q(o`a*ZFc^qmiZNK0YXS!D z%Jr;Qn8SCyCBtEVdj5J>8xyiIPnATqrCJfZ1(MTo_PNXqRK*R48VQiJcAfI2L`R(& zQWxpGMPa(kZ3+3OmSbHU*&4e7Ck%5mnM%lu!)GqAE=1lXlCUJk=sc1GM(=KhH(2D| zF6&k%>KC)vv4?qsg)OfCPUaa|w(Ynh;p6&3?mgFLO2z@`AR=8Ar`0I^b$Qw;fARTs zrvU=WNkT_=;0IU~ET#~%NZ7An{7rwR)YfyEQQ+Qn!=)I(I5tywcW_PL(IRMS&bs!c^<3}eR^L$SNFp;K# zfV24-;V@Sr;MekjFF1on2q?5@MGr#7*?p8M88NNx5`VG+_?m0hD0DdD8dMHQj0xQ} z$dHcROMrE-#|Ci@Pwd~b%G7&&xk`uy*UWgUNn9MSOvr1hhdoJE4E+3#QDKJBkVRMJ zWrVZCG00AsaWmcaHiO#ptM1%b@}^@#40sO@H}fYPVVhV0-%YXdRrx{Bz5_jbDNXII z>HVu$lPa*;^GNF>ORt9v^-V0F_7SgR1;Gl{6M9t81i}MHGG?SV@fetuC%ch|)wIt( z(ovNV$3@|dkraY!E|aG1%&s9%2EqKBmv-tYMz44>Xb^pe+c{Ek`>`D;j`Ls}r}VwE zwS?(@PYt`KizB4Be7WmoO4r{o9=-)y7w}s^BH5F%uxCy>XK)AbfWWP1e>@1VF3{lU z@UZH_t@b|E@Euj~7cKS?-Tke@aS9MvSsg(yx~u(zge?zsoj0#bIT%<;?VJQ5#O_Ve73VrHsImJco^S_tgpcX6 z*gkNzE?vfVlqtm$BPJnjvH0rC(++B(?#cy!?B=)|;Ed{To{LDPE?Xq9gORy^(veCx zutbJ%d#!6Xn=2U~_Fb3k#|439`@tQC}_tnL2tYH!EMl6vsa>UldnzJo;lk|!Jgwp?kQS0&ZWRT1}quLMB8&UiP)@; z?#t5#H{ye;J#Vrb$)10t^;yTOM*rHmSgH)Q$O%c<0OyT{1XoB_lSpuum?o4nQPeO< z&bCxopg_I&%Je5~sLAw6978dd%2ms5r?UM}YO}Pc>r9K8JR7dbJJ$<2eagRdRae(CT1mtuwM}PND4$ zuhNtEC8~3ah9hUx-f2|8gO9xSM63G9zdrXk)AqFXL|kO6`j7(yH*#OEZuquL>C2FU zcwfv{PIEsict77ud9|e6@ghn>x%nyhy~7519Wd>P=9YcN&ss;DK}Xq+=i1(L|CyVX z`#ZAJ5-_dGzOu~>Y1?&&e`%iZVh?3Uc)p>wVC#e8knYT`h1B|1>?X-36h!e|Rw;2IB5~2MvZ*ca={|Sj6Sd@bsUho z3J1B5q;O=dYD%b}fK`V@yfqCBR^TGxY0At=xIWh>zK_)Opl%Qo5JTzxrpE3I*K^%= zm6yg}JAEUj%t>a3Z@H*eyl9%R1<7OkLW#q2w0xI~Y)_Q^L>eeC@9u6qX}Uxd=b+z7 z#5lg{m5e8^GP|2D_%HQSZ2W1~gD$Y@o}H3n6nF|>5( z_aM|Dl8I4L2&UGv)Oq=cR=U&Bn>UF8UVZ+priSZ2ubvdX5$j~y$kDsY%g!66WQAhf zi_du;4WZ(J*P$XAGfnhRW+Jx7fn_e9kcbN5t%tGaIgBrL0my7!ML9L_b@J5wstgOk8mV7yb;Ew*AQd-lRetw8$mBav_R+sYc{U zu8i7$J=q15I2-@PI4XLi>az-VQ;Hf5t{&M(;LXw-?z0EMV+zcwsi4p8)|0?0Tbmc3 zb&dmlS7bjVquGTD_r*In-v&bVs5n7K;5lHuXKtPxk3+xEK=DXFNigS83S?^GUr6Rl^j*o(Z=#-}%GpMXe_9g@fW8?shEMPn- zv`1p^#Fd?bc(%f|a&5!89&PYogVgf%2Dv6QO@)gLWn-o$WWW-pAk^Wk6JAX6g3*=b z&h1#FW0qDdnmdmUAG1-9DPVWRgC~HAPlhrDBa6P`smt}gF^WdUtJCg^cGkhj8vE5> zn{TDMrYdH!{g*q+TI4W~B!|H^d(`TzzC)R`3U|80swF|IRVXNas#t>AO(e3I#b=$1 zwQ1^4{j&~`&~=Ga)DcZWAn1I8Ve*Q-p%lqn+p1f!%xpDh^Z4aO)=OA*h|XE3^Y|sv z&<9@k{k3JFWR^nAbRkj~NClt+A@&2Te}ZdM;wRT5bi$KCZ6a0Yom;Sdk7TS_^p$wD zKFIijMT8{-p0GmnTM?4;=j`zC8T*cmpS7MIwho(&w!Xbv$B(?GIz5$&ooAvB%<_C1 zeERX^qC;$V1p2$%xjDP++LJoP3yE7;mBD_)KJUuKwCmUmI169Nc(Rd|bL3L6KM^a-rhEP@tvn{Ofk<6bPwN zJ6@Yblb~E9?R1GzX-?XlpkJ9g1BiJllY~X4D$4te<98&p1Qcq+;Q~soDK{ib3>TP{ zY^m|ERsoF6bdg_-aWoR;(uf}=Cg~)Ut22h z#Q9tz3RnT*$2T@KH?4o&`3iCVS=V` zX>nX=XFPGo4NzkxgMID%b*U=V##&71TwvzWWVTPj(H!S}cW#fzlA(`Fu~bY7gnsrF z68gMG1(Fpaw8zXfV+k?WlIRSoHuEuqgNazZ*&F14i}KPHYr7py%BlOmAw}wfCAZAA zP(kK5&9)f9`*j6KmnYQP80TLMLG1Rnh~Xl#KMiW%BA;E< z^FzRM)unm}cwY2Nh22^YQf97cj{m}9Z!{c~g zdSb-(4^;4&8i=sG#9lW(lNE!~IId1CSGlhjoDVdaAX~ePR!tNeup1H$siKj=2#pvX zu_Gc}6AWS}Qjs?|G%|$~$EfTpJKN1rNn-?i?b1)yj#E^uub8&G89OAg?{Np*W#kpB zKlFl>>oKZ;ovv6?AwCyCxG9wN)P!+hMe15(e5T#Rg^*ttRD+v zmHCzhh49#(k=PLs=8Cbj3EdZrH{dD_&;fYC`|as+5PjkH#*f?#WoFh3VyP190Z8yO z@g7!8;+w4|Q-qN4+4Y5xESDw2;@wrrTEDQv1y2T|n9>Oj_M9v}ebf>oOYMOJOj+3P zL0SBe8PpcRxsUe{tCW;S=(Bov5}7vt)oX+m+lHmI+gqj*NX39WNlVz1Mj{ZZJ!_Fu z^MN5Y8`p@=)0c3BM{<->18OqTa;pbNy|*zE-gt1 z1H}f^uuEBaW!)t?(@Zt{6tc6G6ikyPRXFqYprAPW~1KSdT-2Ze8$CL{Qvu(K)sHe400> zT&&cYR)V$(3D<&S42p?*inK92h8%%F)Yly4tYY?%HGIsRaU#LREu_j#?W99k;^y-kj*(EZl2GH;6`D?PD9{~rsP?>qhH{X2x zbou6+Z;nZ~s9ba#r2A3EtRt0q{|ZO@%{LC0^P6vSjN%B?j{w35Qa)b@a{lV%v^7iA z{H@TS5S7$`cnmD)r0pzY(sdV3o(xW6Kp>JyM+zQI0OmN@A#yMT(#t}X3YnUw%nVA? z?l9}Ml&dI(xv2KOsJnB<_(jyHS;iJ9yhrvVj+vocER29!Z@+1vpMM=F5S2Hc56Ubh zp+IUd<|+XE8yc0ZXe~mXfk9v{;I-YsfJWkgNDP<`P)u=628t%87)p$9Y>~4;B9&#{ zg+0R=MD`0i`k>*U)(SJQ%$|F}e6CYh6)6OJb&A2QPSr+zN-Yai+x77Zkr50a0DH4~ z&Zae-DHoYymqW$$P>Sj@`?BPj1Qhfr1F#g=WhmH2dZ>rmXY2;@hU(PLhfoa-7GsdD z4ojW4D|o^Ur{XmebaH_=r}^3N^c#HS*2O>!RFX>Fjcaq@ZQcl((;?x&q%rm|;7WadUjse(VPXQ)V}& zc{FX^+w`tw#dJsUF&AvW-sp&m<>u@4^puFEw8sTs>NVXm)=I2o1$5W)dA`CD!VMc$ z^!nEEtkuSF7h30I?2n^&ZhHpRjxulwM@jK{krLB zKfm6}XbUT__0g_09<3#&vE1r9a}U)Xpoap#VbLk8!-V2?6Q9|S4{w#{?4@yK7{|h} zjiQy+4;OQ z+F7?6)pUt(Zq_F^SD6PlQDt&Euaaj8c9Tj7_L&h+P&39skvafv* zpvH_B2DT^v_vhCOQ$p7?uB1zH5tr-*mSBi3lt_SS*sE?F=f!nAxBO&ZQBi3ON!Qcr zvBoveG&t+*WlYh?%!O8*EqJ-1p0pHed?j7Cd_OFcbQ##* z>Vw5~QaWSvLq_sAnu1hWbx)cut(IIjjjV~bn)YKX>p1oGT5~o+y9yqwng0C+anFKv*m;l$)#Rs82!x!T zzt%OZ^WNiGzVjDs@{x+$Mriy02y4?mD4|!E;KI8NGLhL6At3I^2lzRyL?$H67Iv+Q z?NK!;DgR#gU!e9^rRjx{YBdj8io%og=!rkvB005vLEG1v<=V;O$;HmkqsdvS$T5+Jq);_tN&3ZCi1CmSEU<^*fEnb#+5> zaf>c^Js9XhG`2hcwyStXG&PxGvCF(!g|)UU%EcE=_L(*wOwCBEkwO()V-)XY_M8G& zH<%NU2Qep~=ILcu-2OYIv#d2U;TnMu`E;vc%T=526t3KTny@rtv1M}08uz*Yi&nDV z${v4QdZ;PWT8Ms|RVr8M7Y{CTkw?l9o?t8(9@0rnM$9N4g$n4OQkGmY!A6jmdn$#y z{GsABIPaX^Y06Dy*vv(=@Ch>*H!_skVBE+teqk@T_(efn|GXz;M1_6$`1!M=KmELS zYq>HNNWj6tIY&{fN&;87P%$VmUYiqqTr+Y6B{&+e4;vZDg)Y>E#Mvr?m?h`-!V&DI zVsAuiJx>;x5Q;esbJ5Ph6c!gMiGz|0o*dH4l2}wun+wC(MOj%pm#q^B62+qO1*BZN z*aM$7?42b9Xk4+0FILR+@PT_tH!lw^u6X(bQ@p<#$8n~Q74emb?`SWc1)LC{fzN&+_}DJv6`NGsDt7Jfbgxu;@)m1DFVP;YHvFJHdhPMuS)MD%qfuV_DY@7{Y}kqM#uE`rjn-lZF_v=FF|*yatC%m*d2`V+Sm)QKL5lVHTIZ6?qC z_`#o1u!%z)A;TlxpBbyp>9r!YH0`s0&6t{WsG>`vYNx01Dx{b-pk&HFY#$`GoiIcp zQC9JYX=GRoO4*EaV5W(Y4m8iU+NPyn(@`qeIW^Uzr!Fy*!75I6gXF#&Jed`ZSR+Rn z2nijBtwV5J9JLM$nyCG2{m3%RTUax)Y{6R{^z+^^Ia>4tW`FD*lf!B>#t4z6^*9Dhu|nnQv_8k(9wH8?oE{5qa{?SAE|?=1q(}uZ0Lto5^BzUhL`Nh?=_+~7nvGV zk2c#b9Ln97njg`d&j}qE&Tgb;dj>WU8GMCB1(CFDyh-=d4OvF%COhM7IVtBOc(ATVCF@`wv5go9Wp@UHH z!sYa)9IL$aCZvlG#eBfD>G~vI+qv&F&uEXC;$z=ACD>+RT+1{uOB9eIo0lFv$G$<_ zvjJ_&r9G!IZV`Jv+p5U$dLd5tQI-x8<*bC)8(6smbo#% z+t%ms@bK{E^XKs2!^6X=|2}>C?B(;{9ldz^^5yfR=g(d~{oUcw^JmWwe@70tHi9Le zOdG0xcewGprYHA>e1844{SE0T4h2C3qnZ((^ubzdrNcMvfBy5)ufI0!%`-85viEcH zbFxR8@GWVbW0MvXXZzzG>NH-y$sZ3pWUQPaX`Ef4T#uR ztmz3NGXsS(9mD~PiK5AX9pu#cJ_JZIk@oOX%b8X6Xs=#y{W#d*t_dBlzJ1z4@WoIA%Abg=nRP(D1_Q$TqZVj_6Hu z?0fZn^1da={*Y=1GPzfzR&y_({Q=o$|ClpHu1|V$%<^IFk^PuQCY+Uf`E0E+wKp@5 zg9F=J#`bdhOt1Akwo7Dk;TGF1QSbTLH=oauWI@UF@kfp(6q4Ba~>CXbw;W zzHm4-L@;`VhwLN2F}0zFHF8zP1@ZzW&>0bOpj+fD(T35u;q#j)=^|;n5Y?-h=#zbf zP9{%aP){CRvB`l8O*9Ci13z?8NukCq&J*o2){vrAndA)QtCL4XQmYL0HlDaTh$9)Z z()!ym&EH~nkltfSTKR4PT+kVAS-gw>*Z=kZTIMJYi|hF#J0<{fF{ERrA=opt>cBI0 zu!#ixU#22X#iKr>zy@49RQk9@E@nwg9esT&m3=crVCTH(QSDDUXT-rYH6Hm0t7u4* zf#+ou!UX44b%_=o7)7$=hezqGP-MgtsoE(3+)yj`mK+?sPiib@F6M_>?}sJFL;iE= za2RI2HI9a1)?0Wm07C*e@dudi-uS1(=AZxe$5!b-d&L27a!3@W?aqnPly3c$C~b?G z6QelC3TMW0z60d1WI)mkb2DR>7*2%_5QRgkVwP9|3j^7|IB8O2Xf(9ClmnNdih;Ft z%n?{>$kY_~*##_$htTUjph}VbNK(Oc#KKc?xO}66)5(OmaPKQ=KE)rxfrYic4`!Y? zSiEHa-~avJ|DUPZ;>?B;YR1Y0a9z+dqS%0IqbA2dI7`4v>S)4PtREG~+`wyacgbwK z4R^hs`VOZ!`?!@2pE8d zBi8zNjV2A!V~>Q?1`Q&9+bRZ0e7Z$^dJs!=2KYZpvB;FVbmbHtmBx>0SU%`vz>VEA zksO6UefQtykcm+fw7Ygs$X+PB;gj;7+$Z+K0ofOH{tv|$@IDCj{ZJiX>WyXc zt=Mq^E{dcw%>=0Nd5H_WpTshGVlbZ%e=EPQ`|Gc*fBv%@;8A2kO6wfhV=0f8x%75< z)}x26!MCpI9r14Sfq8eyywiMgOAM@_>`d%nA_6bECAPmc$nVk2Y*!?(Cp=!@Ax#r* z_%AH(NM*n#H`jI~l|j`9x-Osd`w5%Z4?MYIG3>)=r|~3~Uwr_AHBN&HU8+U=V$F$Y6=FED?L<-0-MJ_?KnGmhvGpm=~ z8|$p_FzW#cuDzHlK->o~g4o@!QRXaF(_PVz;@1G=>eaE*mpQuY0V(gqVwZ&xd4Bd! zA|vjD!(y)_+V_foyNh_~JGX=O!kdfz{yxZG%hjdV&OGulAKvkIWWWsBxBv6|8Ceb2 z<$N-G%e#H!2v(uba37!7A@9%jIsM$cXFtxT`pkG^-ggE7IT z?Dh8Y>6KUg;DG!G+)w_Ej>L&lbTYkU({m_{X~C2JX}@)TJURb(_VIgrkmP4CFKq!M zXKF3jr+b+pW+*iyGGY0}7VAO4)ydw;gPgW|H0Q43NS9GLm)&!I6MA{$b>>KgD9+PP zvN#QZG7B{?{-CF)Rbl(Cn%=zcr@vb$=BA}w!4eGd_#KfA>=6%=Ebz^&wTkN3q2>ER zL%f_nnx^n#LpBM-w--FiB)Jo|I5;+-(S$r1N^LsS44;rvqIBy~2aS1%DaO2SI$_sD z8~A<7RRKGKj|jCuB}znuGYt6aypc5r~7E+z7#@U!%|LCrt=}%KfS93r<8Yd z&cODZ_?lRHM1^RNSj@8#aicqSdAQ^hsQ!Vb%^uY(4lh6j^LTd8KhC2r5V#^A!lU9- zV7bP(L2Lnnl@(@-QwYnS|L|Emt4%~RM7Fb&a4@w*F{)*v3muWriKRX@!?uy_8RFvW zkO%VjJtimdm?ja6kL`Plp{qY&ghrzjVgcjo{BYVJrx#}=R@`zS+1JmSk>*YOyFuQ5 zzIb!cAk0Lq(l{}u0vQVj*wjWUI$#Sodsz(4${weDfJfl;{I%6MiIB+vDo*h=y3_Cp^b&o9@cyOsr$p9<&up@`b1uiXnOofy?|dU;!<>1omZ?z$x|zovS{m z*wh^SDq^g-qb*ThvczupNGgwylLsHB<*=A)SM+H^x_@wg-~Fp)ch^B{;d>9rzGkB_ zQ>Sv2TD4~HU%sgq{K7D56|Nf5l5#2JW*v>na$N#-} z`SedmQ}KTxUJ< zPB(`g+IkdZBI0G1rE-3v`zR$BbO4gvOUojnH2Du=RF*{KMo{XvB@m@*!VFd%2eI5+{We4$`P^4gCv41S7Arn&a;Xz$2;&*&7Ta)#7rG?8P#=V)*ZR!C}+H##kvI zLQ#xb4R7^77C`Fu^6|}!BsrM3Rtu+|O3jU2Ir$lPd)C_v-)@Txek^kdQFwEC6x$$1 zF(%kI@NeEI+T$!8i9NcCQ7rBDZo8^O{aO0K{|NDp;TH5(!gLt`I zZnyuSk3n=#zsfH6?6(+g1l{ArmOWwUfcSxygxRLA2O+*<0QgL42t_ezhf|=ii&3J# zWQy&8%B9y8C>BJ`st3z+U-Z-(3ox%MBxR+s&0Cqp&sANWV$#<9Is8}f#Ltgw%g>M2 z!Qy9fLEAc*|KfSa@-!j0JB$r8CvxH3^zwF(?CmWO#4DAFQy(zyM}Vm*|H!Dod~lkk zPSE|xP`QKWjaBiiFw~^SBARJNv>cU+5rA?EDSzn75`4ssuUWx0*>_6n>yH=qS+H}Z zEIMdCI_rl#9q1&4Q53XZU(v(cY9Th;4t|wbw$CgS%Kpcopidv0GY?eKc#GVHF$2fn zRlg(@#+e6{KriLoq%hXfx3vd2t^b#L!}r!v^Oy4<;ElTaJ^UByAqpU<-QElmUjqo0 z+GWSBd%QXSDz?9LS}V+K1>xb|^4^YV{c9Y#7*fSzUq}Er7#$xNvEydh{w6@_(}eKR zC^L`|0K4iKw~;Hr4Y4Ofu+(`7hb$c^T1>8(CK+HbI-UONl z{_@Ze^J4EJIU332Jx&itYvjMtk(i;!G-)1Toq#Md!Bhd%hxf%B8#bN;lOkLjB?{f(?ab zDd1a#$-8LO6=Vv%&9%WgD2_q|Wj=}79hZ~6HOKZ^Zr!RNtIMY?pw;;lwRw>ft{`K} z{nsPQwg3!L4_WLOuJ!2E1ovhFnlBuxaIMiC!iY0xu>CJVg#1hUzqrPrIsFUZfIytx zl*{6gKTkrTzt*GA+U~B1h6r4%R)*6SBuT-T>^m?+Ho@XeJ@9LfIVIgH=>uW9z9_CG zoIho|v)oXpuMRcCviNxY)l?f-loOZR=HwS zBI?KQ&p!Uu596z$>%3WQle8sqKq2*;HZ@DB!nn)-pS^cmkK0BXMdw;ifnnq|YDY~S zEyvoU`A3nR#NXJCmZapLe`WHIDH7d{5u2b1P=}HIeftsi_1+ij71tx|4@UwZcfu+L9u9n)Z9)tLMI#MUt_N~{p%2cuY^GoegyC0%k4;-<6kbhwCPqZwfqT#j8^oGXRkGD=^P! z!tcsUWwhPP_ztHg<%dTg5bjp{0jSEM3Fr~BVWWyCQ+F^^a2&6+KVI*JJ1@d*({{?UX_#dN9nI!1u>yyQ-4Y@n55CvjPb|Q9z<)}S5T1_f>*aWjH#OC4^;y;`Pa6-s6{ifdZQr( zlGA~qo_7sb3pGIOVX1dw|HO~*=F#m)S2z8k-Ho1RD2!jta2DzHKM>iihyZ3A#x%XF zc&)!~D8IY$Btog|-?fE1;w=NJdc*97rlub~NkTdIZ$wG5!+MG;4}8IrkOLi!9Nsir z-FyeiqX4uNlNr9@@$hu~)w=BQ)45GRqiLJFZF4j$;~GH2Majq}^8Ox(BSG-#6WCzC zgbT|k1b%H6N7>pX1Z5^PBo6 zKO^BryrDiaOaY!uP>lG-c@<-gAH1oX10lafXDAQAl!q0vuYQVS#71VC1Tw+Q?&MWK zatI>rL$_H)Vcyicg{;-Z6{Ot%x2O4U+WoWg(zur1?TNK+s|TrJ^mQ619<^~Br7fzc zUXRENC=A8RTRp=|za ztXJ3To~|~mQTX?5(?Or#d!b|QD`vcQqTJdaMLe&-hs?cGXU0c-|KHdD@wep5jlYF| zOTHZahyH&YJ{|qX`i6lcL#k=tIwl63B-;87ES9kG)u1;0buxAK&5E5%X5Z?=vJnr6=ZVi7?| zT`iWEWJ#kd)|l~1mijwSuV)_Z_PPrzTm%18z;JF8v!90|_GJQZmxLG4l!P%O@zbZZ zzrefI0=|NG9PoD$nQ_Qu7VVI7u5LQ4i1tyQ9`~r8bt0+3=jcf5ppnHFR{s3J#u73DQ6^)xy4U!H}oNQh3N?i$#vY_ zdn5m-o5JiIrjv87QW>dBBH?k$Mh5MB2hW6R*F7n?&2fDMmk0YWi_I<+pV^Qyzt+3X z1IF&NZa?q3d2koa)7=w1pGR5p24(QLn36LX+j~FjxpW5Lr}x$;8Is@%-fe<)A=j)N zKQ7Ctr48Vg@Wb`rJVia@%=pzN2W}taJe<`e)@F8`nc9k6CqQ-)fHERLi6pxe)WDe zXpM`qLjjxqU@2mQL8H>OH|>`StE$J%+p1BbknX>5d*Z3i|FYhSQ`ly$yW~&^Ji@-Q zVTLvRTNZ$gx|--GuVOq^lBvdx^i4(c5g6~VzxfVxOih_JxHSfU3!ad_aazJImEiZ( zuYz=)%qyo`;EiW)_zhLZ;v(>t4C=*1C=`E6zev554TA>vTF=rYA~J562nyvWB@x@@ z@JO5wcsk08@mTGI3$N-RS5e;_i-k#;0DUCV>Dw@x#LFSezpVW+N>RdQK!e{-1U9C8 z^!ian@}hS+REy9mc-N*31IE{eI>c76A#ss1pQN`C#NOjT6_Yz9DU2i{z=6@lhwJM@NPHOt@p;5CO$iFiVI#wr=@fcL%= zGZ4Hj36;rW?_}t@x?l)!o4_E_7?qw0(3Neqkt#W(qsF-vn>*6Wh`vmin~j8s&S0TJ z88*Z(#057k>m2)iL(|*0p}vK*!X*9s1$7&N^s@^K$LM zM`LF%d&R>o)Ci@U(=y$Y6}@o7Vky7)(3`KXIlONeoT028pl|qmqdc}c1X^5&VF0Zd zl3dPb(KTBc$*VT}u9qvciX>S^xu*?aaTSO$_f0;_aQjVN6E4W>7M;4Tzn$plXWXEw z^@w*}vhGme3>dLrD5}Zimm}jv6`<64Eu)YI_1zCm_um*M^{(8T=h=M zLJfXD;W(MW_=@wu%7H5PWl&a+(Ag{(0>CR?^>udaR&?Z}iq6Sp#Q13ytKsLR>u-md zh#s(hMJkNObq{q`H(Q2dr~VvbI_eG*TS#oiF_nu}x7Fy~U=vnSX20-tTd1TsM605a7Gb%%{e(4X7&!HZHD9;(`;%DevT{{gnekvWr>-sKB z196RBCPk2!A&HDKke9UD8lA&AF75!u;OjdINw@~UYhHpU&tmT{p}qts<7^SzFXQSf zqQK}YtbAS#6&;f@6Fwginsb=Kssrb*H^J*yCm`oH`5@tTC9ZyM#FMZoM(rfUH(PO$ zjIYIF58=#8%5)901Q%OZU-4^_d_Ogwp$ryxTlv*=ON6r()NXw!ql_YY7f$gPZ}#^` zjPUc<6&)pkAm{fI@dWH_%R|H+xbqb_0z2Rq>}>z{&Q~L_F$W*Tk@6Mz2;PE^;9Kw! zT!4?@6nq4)I9#i}7T{e8cl&lHKiCFGVE4Zdwnt#&FNgtS|9s2-xnTdCvVUG72C?~m zujux-aDIV#=LSYZ+Buwy!x4r&G+i4eY*D0c1i?TQwZ;#7aRlaNIsqHz^&wUGBaoX; zBf+Dw?WpEpKue;K>}o}l55bXpoAImz!=Ps9(oQ>`b+m&HK|;Ol*zLeDs2#MS9s3>a zm<~ZQ^|j+g2ZBM(m^L(HucH}r_j<0xkf3V^!`#<~y>{qt8=%+e_d-ADpnVXB<_#R$ z?cmVW5F}R%FzISgGp-t%v12u(x^l$J)+Sib5)IGcoZs!&TdwOlEMjI~O@+a2{2Uw5 zc_pr$y=k31Iy{tf?MKnRbq`>@y`Q{AU-P~2>xqj^_8w;hF2F~?&pzykfKAQCDa}uK zD3{Zyi00PB;yc6QA23bxYBkAW*wc-bAjSc_Qxa4r(_pGetpo}4sM`h+I*#@2?3c@NA5kHj3P?o@qf?dBASpQYy z!?eibA$(xvAA}DKk>o8C$gYuY)P`;2Tj5f-8 zxzI?iW}PV3iMekHlJrsx)LD6{;sx`_I(h4P#jL!%V1>Uysk{yEiwhWd^3%UnTQbf) z@~vP50$!r&Lb*7@mw`?=ydug-9Qp)OMnTU=aAn7M86aW8UXM6WC{{KTfK6RDj zi@Q^dGCJagI~Z!c5FsWH z+ss52rPA)Su?5o#7HM~U6*RI@U`AklF@f-U#~YaVhcUdRFylAk#wCfS`kLt&F??u( zU|~nXtW3@}B0gtgDw!Mq`^<0_l@ZR|6F=p+>Gqq)r9Cuqikfg{<)z4FKT}SX!k6;9 zh|C)KKynx#x*`zcD1{`3K3U+m5>CrB6~5fWj&p5(*s%L|R4<{4a=#q<&iwkSn|h$i zL$8CEFQBDy5)hRXbNl_=D#va0Lv@RrJ;IBLE5&b%D2Au7KsY&vG0qagQLF0_2irc5 zw$`ucnPEcTiMWD7&XoXG?`y)%6=q{v5rYN>ghy)|*$DYN3!Sf0$0%!3D@jI9#kC+^ zT)z@H7Y8i2I!wUg)uC&#hXh6iPX=p@CH3ymFw0F*)O9M6+}Un9MKbYm-)dM~TZ{w1 zc-@56thoSUkr&KDDql4rR5f}d8`MoqevVVbLj_n;Ctn^eGWy#Pq72a!apA&ZzWv@* zhaThy{UXc>M&Du4Tpj>>x}Hdqi_}Y5}G4uMrr;n zq=&VgDzWEV>nCSVi*5xRU`QF?zTr;hT>`XkaC?&J8nyTxTE~J0_L|wEWy8q8D}2tstJe)s z83Y@(iu^QmRDH;emkt=ulz5!V#Q&@ocRJPqt5Ly>gB->bUBk+>;U#&LgZDG@6&});INs z_WAcEeQn4QleQ>rZo`Wg+k1R&zst^Wq<28G!@)g zc{%2vdvMC*s^Bz%8Pnfq>7BLL(b%@iqS3Kxd)Hq?Vt^&y0x+AbHuHJZp)YWbadu-9#b&o3Jtd~y`Z4)PgEm>epC@!L@aZuJz zTUC+OCn)tE@fcz&&pqHREuyiQ#+nI(G*+2{x01Uonxl9qaW1oj;_wYM-|?uw_GqZF zZyD>xg%Ejz#JmI3>l%rNdU=K9;x32Y<7VHzY2`&GF9ce21l1jvphk0(xnJFayKpbT zc@2BtO}GxqjT?F`eJfcUaQdxzZp!kFk$3Rs$9kxE=@2Q;z@36e zBI0$Yxo(_gWyx`dv%5Ji$qyu&*1EOARWCaLQHJR;_e%TgH@LWoiUcO#;G!9szxn`~HShI+ZI1_Zn2uW8|8u%u|$Ijg0txfl(m<0ke8Up;zX6A11i~HkU3z z>mpjyuM>*!Wrn|~jikz@^wSgab7Q3jqf&U#8V&vQDZIbutpD_hO@jp_g!|5Fu!r^K z?v}Z1r*ZCCwyPX;()re+OYD%!8WDLZAq^u{RXdU=Uv)GM%EH2qF9|HJqf3+`de^AL zG6=#bjv*o3t2RTm$)=rH3yT4X({2EEThZ7Ki!$YjwIo>*45z7b?plN(T0}9A{J9)K z5)3Cufn1y*_#-SXF=5@j0=g<_nmhwRZz`L$bARn=&0$fFyc#OXk#TLL#jMi>!^?<> z^kzYmRaXH$0{q^A@g`8!`|Y`E8dShW15y%^*(HvO`u44TT3f|*6{)Gta7kgPYwHDz zh_YHWRF`m%X)6@g&h!cQ)daCjf}erc20oqPQA6b&giGm0&%N+-6dH}HU+*3yErJIN z+$Ra590g{mIn9#M&}9!FKsg(%!W3u|(HIx5xmcBvv&vbS!t=X~Mz{lo2pMk3TKv$oHa!-6c1;dsM=Ouq1KeT9FMEF3b1| zisgm)(>H_%DWD4W5-OjJA%`whx&;vP_US&jJD&|X%P{uT6>)u7L( z!D=mRy?@k9ZdIyXg}K*#c$)SjifhE4@?=%Jto z*gVS0C}jhnHxosbLR1+vT3YCs!6<=+Z>lzr5JgtIby4^)@Xp*79|W|iZK6h6P5vsU z-I4SMm3Vvde!*QUMHrzGCBCf%yFj~k7G-)^m%|M>3HtLdAQhqo*N-(F--)SnQuuXQ zN)^@r3T9H(ng?6u4OshOn&v(ZxjT;act-YaqX{nn0V~eal6WtKnH}NxG+9jeRwI6_ z2?K<$`H-UR{eZR9@~bJ;q8v*Jtl);B4y?7@NKD+k*jbdGU|7|+II%cg+IdTA#Xr8C=ETXXC7*$bH z735N8)N7g08s^nn^f5n1D+XEn%vpQiw5`?@jm%5I(2!)kC}YA9M>#DEn4G~6B_#Aa zZkM-F;;+Ej(g5pYrGplyj*STdhmdF0aOkt4EkPiOCh&iju(%VJNvqUaC8g-vwFeV2 zZ1Anj;4?|U#U(FEwth$MOCPEAK zT<)(ENGZyu!l`l!X+@X%5-=sB90a*e@_;D`{2^nywSuD|2l}H0N@T|HE)orJ3-d*x z4>hb6dA6?lD!{#+CxOd`<}87~UHC9KxNl6 z`=>9W0ejOOkfrk`4{bD{1!lEA4RFw`j{Yc3@y$try#b;+o8hTFWGlX56xbBT4E(A5 zsL>2zb_0a=jjSc9mYVhA&H0%M`Lqaw+rTVv(9%IP)5q_kxqq*+iNvbXz*CrBqWS7p zY!9(ktKHF|0_uaYz==^Wa_2d{({#3;z6=M~x|Q6{0Ta&3j+}ICqRS|*lD7|-GeQwV z44$MVp|CiQaSpYduUMM0h@xxw=IgKTnlJD)ky)#|tfAdOuk_F#I0?%+V(3JykS#mh zP#2t)&J2p``Q8|6z9jPlhQT#C>|7D32Yw`(2IgmAUZSztkgJXQNPX6c2|V#bRi`GE`= z)!Idk7}Qk%GtelmpPh1Fu(uJ7XTzr*ABYv2T-Gj5h5g%z!kg%>?-@{4aVjbf-FmU1 ziLU8HDvB+$$7GqsUIO{`)aw2zn^v>Rva&C)^5LV3t~U&-k$a}nVQriCSNkj}EPGX6 zBrI9n^|O;>QbZaC$1ro*2Gih~eua-#Of3G*o*)wYRO*=u#eX+Rw0C zjmpB1Rk!3^!MM8B=osVmY>2{5OrlUbBy(C2Q| zMzRSZ77!MZf<^3UMMB-49>NBBe2S9WO|ULO^5atZ_fSbx)qc9a=TO0?Pd{t*vu^hE zP_5DkSu2(GnjkHqR66_YLq3!@AM!z45eBZyvT);@cVgppKHnX{zKhylo3&OgEBY&& z*R?vI$!eP*Us&|Mb{dZvnV^Deg1e(WF&`P|8y@t#6EH8<->qu(lJxG|*YEzi5;IoH zKsH6$ZGB|OxsSKEI{*5N^PfqMKx?VBUb4xVv)pVbckMd%Ee#em*{n5Ci*HH_o=9** z_aOyOY+A%tWSut)+K_Xc5Ew5ZeiDMmXpl%2akcM>G^l5_79SdNTe~jz?cDhsgnKBi zXa#}Y)d2|XF&uEOE!Ob%X01kj`8G#}>8tv_$-^1v0(YNTvQ~1S>KSYSON0yCy~Db< zDRSm|OX$q|Us2>tD;DF7QrGW&iQ#Hy%YQ;R@DOG(PGE8}i;5_wJjb}z)^!nKyc)*! z8tp2_6q|9hBq_X!ibV;k>UZ5* z>p{*&SU_PB@=U~bj0~reJ_a^@UTrB4DfR~NlQLZtxOhwW@NoQIWk5eg40u8S#o!4! z1O`Q{!~~aFqSA+i6gOhy{M-`LoYKt{ahMj=g! zkxDrfX`lFQ0QGmk3X;0R@KQX~(GR7gs?0aVJ_>Z7jhj71+7v-U0z-0bP)?>S99(f*cm}DC0_Zr6ek_ zoJRd?5V5Tv^tH$gQS?s{Fx7jd*%T48aH}gxcf(Cs!~3Dflo6#UBJvL2IV~azr;ACE z575vH*#=!@#sI~*fU6*`5!yl&@(@&}!d-|a>I=i+aDk@8mk@Xe4pqNVdRm6DLrDw^ zdIkwjuc6OO?njOZWMP`r&M})kH^F*>=aIZ=!%&Ey^+Q0rNwQYRt1j9aiAwAX^SS)f zr=K~<=C-xA&O;!Hmgs#yfed7)h1a7WhG?CxWtf!>t#RNjGvxN#hFV+twVvP|7z;@E zLK{Ik)P9=co8yzWufD!J^*a-_Ho>Vr>?p|~W1><=c<+h#s1VYkBorpEPY1W51(YT3 z{ZJ2`6dIwTjAu|AY&JFmS{Y@%GlUGIvu`4iWko(O@a^5HHI%^@!9iM@cc{$)0K0i#5gXD0awY-mf{ z{N~$hY~_n4*+Z8Jn5%b_hz0BzvpqKjqOhq!=Ev2x$b`(BWYMu?n^2NAHp%$5+7=mr zd4o(MRxuk}TgPYuPwEM!nsbRU*z)sW=&F}- z!Q)h`76dVmKBR)g$k(HhD3sw7oZ^C~^7HjHqezzF1gY}FPv_vZ*ENnI;NR52Ntx#j z8Od7Vd*U%Q97}aYWMUNc4}ZCg0P3V-O}+umt7K6%^(A)cC&n5;t1$PPJzE6Yz=Bek zw`_lQDX^~CdKIRo=z6`dBxRI-U(&p6=UG*3$ZE)B+GRPJK&Ele(La4ge}zkzB)NVnMwd2i7h-cl18NiTAA25+4Rk zmXBHag|{6UI|@jPyJJe>Jom;yuVdylB3Wa>?bkWVUY12f{dr02IOix!WcgKXL-~7L zy1349A|%Xy{T3%p)3n^e<>jS=1OGeXr#yTw*A%M$(#I?c4OPWuG(q-W zZs+q#+OJ^t=7UvXW&8ln80Dxw=)j4P*4y1lDX*T_s%x$;Mw@p=UvyHhXdW=NugDTL zBjm=gplE{Ph(e;yPgWKm*92ZD;NIxCUNRPnjcXe;I#bbcF)in?mfXD>GNCJ2(){xz zs$Rmvnw6JM-|6{KCZ%$PdsbeGZ{HYpc&#^P<>iH0sL5eXue_M$9pz{qWziHe##CM& z8|wzOPN68_vE6VMrif6O9VbcQbN#q)N`J}s@5%xK4XK{#vW9kGHc+;##FI&ivZipu z41+8o(tF9$1Ko?Ww#!WnN9Mn`v{7d01K5ah7DrvzIKW51SUobajTX4uVKH=&eXk0v zS6yx;tv-D5Vt+3HUfF2ii`~0^3z6qrH6^j?q)w1tT0}5FleG4OI|YHjxPY`FjC337 zs^aR-?5e)PIn0i86kbOuN+Js7X3SeiXE-^|l7Gw!n*=8Zw#tHqZ2t+&v=#>rj7!pnl?sbV;Iu8u2ic433&IA4x0 z6@V2mrULd$a>g{eojC9b^4}U4Zfvp+*kEc21hI%;5r80xXmlA77=S=UK4FGaNnjza zFb8==$PF%%00dNQqz9vhq|oB%OvRA*v60hRne^>p=T|w@+Bp{zm{C-_!rI2ddBiwK zAhlVf8MiEt6h*&6W>I$^s#Vk?X`crmP=~bVS1A7x7HD!8fZz(=EzK;N{vtx9yenar zq%L(A)zvdIO@~;`Toh5Pd=A@9Rfp)^w*@ZqQ(VxOh{X6B7I$6T zvXrohF2GD#rhsiub!;@Ri|^b%H?Bm~=vQ!ldXoGcsQ3W=cAt9i)}+E1zSmLOhYPJM z6T2173?Cv=EnxhMs$qoOat3n$$`%LT)yQyLQwE543z*&tptC!NUmvb$oTIqF@l{*= zRJ&2C!e>$+8hl?Ss|u^vr&&p(4Begu| zk9ka1qIP`@HE=Yl;_Eza*TWrvKa*}&WUWLo_aCrjN|9WH_8+uHrPW^fZ^qMq1RIr1 z@r!3DO$wL|l09BYW@6!gpG>BX7+M@uNE(HY7ibqhFTn5#2@G;pDtQ%6H5ThO2Lu+^ zupk%8v8wMh!`E9p6G{uL)lR?Wh3-14s<|Uy7zXoHs!to8R=N=URa*30Qw1eIu%L^H zPmLAU0i0B7m4G$rw`FBb(LYShRajduJ3Y|-BI#Zj6)=nM>gn8cXC(#VN2`ZNAjb1N ziYeG2d5Y*8z8?bu&pD7>LtEfq3OE5+uV+ZBj&~Kd6{?27M@=XYSqeEv@fDCCz{Ug> zgld9DJvGJ32=s3LW{dl2C;53!+7CB7H+s(*PIRV1ZNY^ zMAHCX(GuR@8^At&s-YcNKfH_x6p7hnR7Hb4{_(BO+UDPM6`Y$QT6eE-%D`%qtbP6t z%cAL&vS+IVIR-`s^636PT-}veYZ_|t9~nk5htx#qV&~PmgVLy@gI2-zG`ei#^#ih# zQL<8Y_SPt#cQCs?w3@?4+aWmp{^j4_9lw2L;o*lg-xPR$=+MQFa?aqyeJ{T-oqGu8 zkVZ*FBkr=&g7W78BZ7-V@Q0m0^qF&%KUhl>=Wvb- zBWzLv^Az844~;2)3+HcGxl>rgFr(45b|9$P@#^iH*Kc26_*n1rXq)0WoO2rV^e!j~ zKohNs)F9W?RE0ap9u0K$GZF~sTN8~w?Scmv|ErUU)<0OV4g-X1VI>pU>tZwAASHKy~`m#buFN~x7ULwHdeo!T8M;$@!KeO zaOw9px`yWUQM;a3T`KIyI*0pfFjcwXen4v%uQ-=D-vSV}?gisIk2vVg-;lnks5QnPEa}Xe3Y6w;mk%PJ?OTinGI~ zz}qsMbK93SP@`|QWvT&VH40@Z%HXLaVbI8_wbP*gWG?zsHZfNILhA#%yLkXQlN9d( zEd?h!tZhd}ys6}-wI@D;NrC5loVrMb=6uEGRN}>G7I>WpdxG%N{+1c$yY5af!a!l;uMNYu*R%tygc--P8P+M;RM#Bh?{Sh#%GC! zh|bD0IohYowNLh1sJUWy8*`kTOK&FW4_Xx__J>tAmf{?zczSo97cfdra7JhmA?uQ0 zW07u2YpmKw^zQHVZG+!EKEItuC8trEk8J&2=J*)TAuUi$LYk+`;n?2Z-agph=l^eS zZ@d5RJlom%-OjV!gM&XjJ@edud#5<9;I#8^Q^k zpv;)_WXF#ULEzie(!}CfAGkj8o_NaIva%=~SQ=zsUelQ^q*_@_ghcF8v&&xFaZ->0M*h0rtmt-C?Il*db-7{Z#@MUGenpn7-ay> z=OtyPhuFhE1eARvGhC(#Hx%O;U%g&JdIMnw%6wWx2?P>YBcnyzB<6rK$IDr#96tq( z^4v(&5+b}6<9UuVn9)PkygDDwXHfwY*^`1DDAUIg08fA;1Kh-vf6sA-*xcULEJp%F zvl`{a*SHx>=OLMG#j~iO4F($j*2oNT&ovoe-igWo45lz5FqHXel#p0bDB?Z6(E`WN zDrvKY%H-=WJ&+fCn)UzkdS5*q`hS0S`?;h4pY87MJ?j65czokm>3y;8QR--2Erm=6 znkQuZg!o*+jCE6iu?h!J={B(k%HIWY=i7(ssNbQ1M8W=Z6QZ#aI`P@q7_?)d4LL1S zxs|OEkHEn;D*?U$2m4?Q_Q6J($^KF0+FbQWa{VQi%A@se6G#{0RcBZp1kddg<(Y@N1!lk>vUb2IrQhjGw| zoPF&;y<0;K^h|%KD|XJeHGT|e15arq_;30+;ex(%(^{G7tazuZL8ps@k08Z2urPRn zCTxTx@YXpkWP-1&rrs7exb(CfH-iBg?hF4=*q|P!U96K9u4I?A~ z8?-=kos$Z15I6&Bf>yy+1Rb~lCJn*J)3IX?Lu@dc1T}8}vr;nAws|T_qhbo_sRE`M z!^`N%&bko4xbK0icNp~!WiF#lTVR8WlT#~PwiV3#-VS}8;RKrT`PFaH>V`tj?t%j{ z%5yh@xbeyY>?vx&wj4FgbOu$X5dEH@Y?@kv$V;9!NTVr$7HDtc0#g(!u&z89bD2nw zAiKDsKZT?`oQ(YN^7OYS0(F5xaYPUHJ0OFd zaNBc4FSu)$Rb0J%X_stW z@2s%_c327Jnp()QMN98POIeot7dKQe0{Df))`>v7VSPeAmzNzFf|X|tJV+X99tS6*4Dan*6=sFe|s-~>yD_sl#BgNU}V}wY{+si zI9myARNir}`jG{#(XQHH4`czoO)XTTbRONFm<H4*%9QgaMeN>A%i9(CZ6lu^ni-)lKxA$dwbF(}|1 zUqcY3DIj@SAY2lV!J8Cikccw@pfya>dOJ~LM~IbnRQjmox?z{)@pyUFkH`M6#AZbc zfG+$0;9%Re|DQj5JpX-=hYxu^`GFZs=Rgg3eYnRIGJ*4JSe)Q_9u*J-f4trcUxeEM z*a%OeJi0_Fq6m`kJDAR;2@@(y_hYreb-fpEhugtuWZBWy_Bn$9{&WO5e5P~?_@P7v zGi{r;?w4A*=oi+~1x>(|A2?9IF3nREM`Q$ctcLVn@H8!XdM|o>%bx$jC5WSp+ux$? zfYoK-?r;eRq{dML&Y<#?1F|_z>_m6P^BZ0Rb=cOrV;_y{N8@@CND7;Q$#W`Y2Jxf%_g0!Qqd)S<)bX&T=x;e{)ZQQKl%(#cri}&hN=WCjoDl^ ztcDZ_ci_3^Lr6Rhg=uY3K!v$J3K|9kd0{vYBotyiT1qy!k=8g(nb za#n4l7Q`tSS}Z9+~dU!7BB@({us4!R=cV>eP?+K)*J&(QJrBz zZGlj44pnwQDgI3d)Fw2oX-;v9V)O2*2q%4pOKQfOsnWd-jIZOR!Ei>X+6;p4E-p^b zbt-Nn2<#c0)Gv!$0`C;|%f39}7ihXk#6^t=5YDZvpEPx@UdoJ7PHIs0+teNncuf#4 zVxy4rX*B_l{EiHd=KXK_X|(@iKKrg@5zw{%KR9sj{~YWd>_6K75An3v|J?psViB;o z+08Ncv%Nd5#$_zzW;;+|8FKH%YZ%8kqXkY=SiCdCS1_JPMB}Q+XHm8F@juI>6|Vh% z*W>>8^W8mX{O>>R|2@#-cpiBUt$=aZ-gLgjb6L74FqZXYmN3rs0nWg8r^kU~N!SOr z6{4-{U8yO)!v*>$&S;dL;^bJCfWV$R<-4WM zN6>H+um#^GoJH42MaEITx-jZmttc0n8$9~Iyd3MC(8lz$_@vOG|DPXhJNv)8+s}3m z9`*l2Joim;LXDf+rY}f6f4J8j#K?cH4}p086koVG9;EyRUd}MS`n$;S8am;K$bRx2ICnwc%c+{(IToQ`s|0zf^YJ03&ZM&ZHNT6%tRNan3LDL3nwqHB!bnXx5 zZluViDG6hJvk(?EWGoS|*|#frXMFn$yaVoZUh~0Ht&>6N`QCDEHSo5i5k=Wl^#kAd zD@9=sKe^(H^RbsJC?hn=bWnq(9kD!H;?_E$tw(G7>jSo?HOpj)mh@ zieM%;Q4e)|GJdn#-ncV>)Hn2c3!2=pSSH~BxtXDsrzI0!`eD&s) z@hZv@&jRDXl38QqO3f_oQmH77(tH-}=$ASWyj@PjQp63#JO;OHT$G~#)E=GtJ^;a%zX%8v$^ZmWj!5M+*Mahq zk0BSNi*oEK=AaZM`Xi1-zUDEaa2_-x_7rmv%Mz@>0PG?kQX6)E5evI4G7Pl}9!1Ja z5*H|ELKoPm{Y5O)vWQutKM8pnWpyd4@!nI+;XNxc04*+Ply=IRrUE?V;w=_4<6uNKtI4zCKQ^{ias#Qz}0> zo~~9Z*6I+`+!Wz&+~~F3clXt8UO?gyHU``xb|TR=cB$}LT+g3n)BzUX~b1*#I`bF)t@iX{C%XeZ%X-=$z z_x=4nbz*D8bTJ7xUAKpNY1jOCe=Ro}8}@&c=6(?W--G>H{MWs`$Me4jc^bBO#2!%h zrce8ZNTWJX;`M7$2PQ$4N_g>Nd#~n%b8&L&(-Q`OmFkJltsMO0U*_al(K%q(_}{CY z|37>FeEV_yKg4ry2f*B@D*@vMZ!j8xgJ&Isp6`Oz$Gj)y(*mPk?5=*FM;}U9?N2f~ zYx}8+`pbiTn8lB-O8&<`{>7g2m~oqo`A*a__W?S_|L)GgfqVY5`+Vnd{6ECgd;TvV zp#_Sm7~mI7GW5ZYCn~ThLB9CFGRglLPG$O_aOBe~{q{P4j8c^F-OgiGUO27#PhHkm z;T@!`iAjXgvXD-)hv1tCrE79=Sj)blR-kA0!n#@ugP(%nXYZ;P{1ja87~hP=IQS_L zOYmS51Y+fF6s#7+;3t{Zj_}WlL=mXKF^H75R^e--IL2kBc1NLJ^&Wyh6!1d{2@OjE zi`Ur%^FKuJ<|7c0J3`_7P{NU54t@8@998cvBf%UCFz@tkbnu5YzGUW7U^K=eR9tBk zms(K1gMWkZ1}Ib)2~G%1eLPX1kK_L#J72iBszv^or`D+SREq=eYoorUh@2xo04y=Hl_Mf!x#Sr0 z&z5O#QHaP(QIP+%(?~Ak_*-+(u&~PL&^^-`d)<94S6>O-=j!{}w+9=?e}PN7q78tK z@xQmbR~!F(kN#f|^W5KeUPP1v!S8zknZ&0G8gA?a^0k19nc+(-niu%??niZYw=RLL zn}h3}TuKB1upQ9WTUFG0+vS9lvzPq~n&8#hhlj+Y;gBqEO|V`P_}YQ`4T34ZSm^{@ z`_s|&UiduR9&ZN~gFVbUHhd!vk3<3phl{m_aDTc+Vn%)zn201E$A*@rL$1IB58gUM zBAfa-ESOj}q1|v(1DMqrMc(zKp>_4qp*3@HDQ_^Lb@nLPfU#}+4nN=3jxXz7SFYvZ zpP98$q&085#N2xZ^Z+u6lMMFUcIl+2D`Hk%E}F-Myw`1iI`j|EIx}EKi@Q^-7UPCD zfL3I>fN~3HxXfMXo3CTBindZC@HcFQZ@c5OH-5g#MXlzn`fWyp^0+r0xE!tpBh(b3 zBuU{-R8%;OSBs!o^I)M$HmE6cgAxIat3;xYi^PX{>h>ScSv{7yn^ly(rEqlEe>>0i z4qW^1`Q!bshk2a!pOrM+NM}xJK2%Pw^G)lGX+CyzyaP|f1~BzW@;N`=d8 zM@PM`N-x!4_GJ)Y#~PU+afYxd{yY(`o;=uyNv$?yReA8~(;@g+iJ)28W5~1xLHO-P zM5yck9484PMaex?FUw@QbS9uK{l9bI#{YYMaPS!a>p>nz|K~V)sXPB`(fLi=fZ`OO z+WR}L*Zpl>$Nh|d&(>h5lfWBGR~>GvfI4 zweoQYzl#V6WOjL!O~V)$5c7QOJh-@Pwo7o$G|7mN$EUAPW-z`QIpNo=_!R$zOA%?3 zVju!VgCI~y}+?$Pom zb)MUUJvgkhbG%tLf=}o8adC5{DgaLP1(;=Ss($Nj$tdEB*uKh=x7^y|9;)-~G*8B<7qk>D2c^x|%w z9)geK+BD$jOYGk!`VFpeJmdd5e&G0T&vxAWZ~J@C9_{}Jc|88#IUD7K!Yppe^<);f zMTDYkdXh$HK5AAJr>YM95vS!G^70S3J=7I)!fcay7_YaDdj<{%JdIvRW?RbfTHc>H z1XmE|E7_pS{|g!Gy*?S@17fA1B%?OWKdsH6Ux=;I*QX z&TxVL>E46rIcwCf7_Dl%!A_VYXq`?AIE^U0zoGINAPClgFY3iu+O_jYhdCI7jLjJOmvS@WFRo#6X;hvsh>)1q0f=G<7 zVR0wh*+iNOV?xp<{QYq@Ga$!FOWOtllNYBJ;#C#9u+3vs`n>5u)T8XXZ(qOrtG(0e zZ5F(xFymXSl_t&7D^ToWO#u1g1rl8Qo4}w;Tmf55O<3nn!`J$I%`5jy z#WMGc=1}c}VpJb3?OWRU1mYjEe+irrp{^7rzv-;Itrc6;KfEv_bsLt+EMBVe>TZi67@7I6{`r^#y{fBy3)$ z6v?%OGf7KoQ#sRti&*Q04C`5cRV!IU6sBm7sP)$Rp+a?< z?_`Wx3qcy$Wz2B0rU>dlua#}*3Bdr3Kytt8evERSFQWRkh${0~?M5Vgv)30=dHqI5 z&rX-!>Q2&C?>2sEs^@#BI@j|__OTBQ>uS$?v&Y@fX2-kVeEr{0#uS5=Y1Wj6hQ1rB zoUdVnW^T}%UieWib*~3QAv2;Zx{u*oY&`ma{KlW|{r^i5R_61?|JvE!-FD-DvHu?T z{~zG_JbU>n6w$ALOP@)&OunVmX$z@q>Nc-=Zm|Hxq~x)vN5%9IM1Yk^VKv=sDz|@>O##Imcp*Zj0Gz=PJl%4o z=iArs{%W_AZ-*9lV1wi-qHhqPAlNh`h;|-`R_zTyI2wVC-qL0tsE*C>rWJvGI>5}H z*I+yuizpL;?r8I6OK@a z)hr>EvgfB%ME2-FO&@xh02p^gN^8oSwukSF-1r1 z!TB7gnUNowt-t$$v}m(MSJIjv4i-*W#_U>>GJ(mPM@*M~G}!fbJP=C?o%Eyhm2$ zJF?Wg=qF=!jw7q^>*yM94?gEDIy0=oYjXwX_U4QJ?iioX@q*FC{Yc=i&>3U>(HrB@ z8{=Qg8>2HAhik_9PxxBSf0jS^mrqyxr)SUI^Phv~&mYf!9^`r4jep#Yf834#HSEUs z-;DoU-Zh_nPku39n{Fo?hw#thU9-M`cg=eLdH9!l8s>jHU*G4A|GRy#>&Ab7{%q$l z|M!DD9*=cf`bD;@u?{lm3?wJmOTRGaE#7Mn8Thw5DvV9&v74l&J$jxQ3$mnm&Oq4Q zyAX+sCs9h^FQgkh<6j(A&DjFzA@YCiZ|^*J<3Anj?L5YRdXQ&f|1bXjEy{j0>n89P zgeSA8prJWf0we7buQ#5fY<#VN+90Fb1wlr)HIO_?EUX$Bn8_%{*L&f1FtTr$+L^aD zR7T@JJ|R5$mecBy25kkW<&kA|T^V8aVulD%gy0GHj^gbSsboo+^V1xiXZDn#0#C@7 zKD4zP80^Q^Q}8{{N(<)EKX9SA0sc|WbASklz$_{fm9hB$0&|q%VvFx!fanrmLoknS zS;2plgn|g%L`8-7`GehxAFoQEm_lRHs>l9fTCg2GP4MJ-qw%&vB zEe~wZlRDGbce#4t(QyMlGngW3#Vj_8gP2cc;1Y`21l-JE1}c1r00L`OaJH8jt)Gh-Q;Pn{!Mdd*OGr%cgvT0;x zZf{(VtK-&P2PJgd6VQWnrX%WyhGzSzoCA!4CH4=ieua8fRVO!A_2F=0ktT>H*x6XQ3;Cst@m*iX~ zwdm7k2?lUDpaIT``OqoBbw8i38kSQ@U-*vGPg6CoKZJ*?hLAF$t+-~e`{eB9W+VH3 zUp4_Qw3<>xc^KTJd0KCQiKzh2b9x6ly~vt$N2BAIZofN(DQ5D*27hR_n9Ebm60ls> z0M39sl^+wPpAx=Lyv5~K1XvHf3T0@jTrIB!oWk3;9DMN8@%VrJ9GICyTdW%k;uX9D zPd)%W>6Up8i#Q?>#IvY~VhRhw`|AdmX#%D&g9TeXM;SmFPt9*KO0k4X9N0Mf<^=5Q z?CwB_qZxI5>*-S` zDe(xHyWd>0+4@T?BF{vW*{{pV1Qu+b&rmuNWu_EtgmH>sM!&|%ohWiHqRgU?iYT+-%pXD~`S3!THPsgS8P$39sl;*E= z-d_;hz{|k-Oq}lo#$^FzsZrA;PVgM|7|7BKR1INABRvnd2TaioqZ3BquBCJ{I7IOh zTA-Nz$mWiS!qbS(c#cfQ#)Z{a158())tJHC(ypT$m{7#wj?IL+#ZvJiFJKH4n8mQs zTJaU`(Evur${R4CH|6Ll>FkFhZERe34_a_m((cBkH>PR+22aU|pQq}!rg+LfdDgqa z4egCvVB~KsE5q!;6i*2=l0|Lt6ESPbTCsu&jA&UvPILxg;w)$twR}ywP*2;lX{|{~ z8KvKsG%smG=ZT`JU9^w=qqWT-g7>-z<_mukekfsacN!JZ95O3aZAS3W^j!^U2Qgk=&A zQ*hz{@@fAT#CaZoOd zoYqP(i*W*z6TQVQHkV$B*XL3745ioY z5@7yk2?^8LZ=>RBSy9I62#`g%aS!|lNo%bGfDgO?4?iKIgbjM#dmM;l)NnXT{BK^ z*YQW4eivHRjtHbEh8cmI0P$@N z#}>c2>a+G8Hw}5Cwr%EXMsd%VFhNr$iiW8#+4Y|@n1PZwJ}^SjnpHQI5&iu&EDDsk zX1;0QGke`yRG4HcXT0?eZ@gJ~S@XqfTte`R6w-I?1ZRZrE@n8B>o&ar01*M)J(U8S zLEw0xabws#V!i%pd;H?(FGK6U0Bq=N>Ves`k@Do??6un*T6LkfIOkiD4X z_&^#EJwA|&vE920u6Z?DjTrfb)4#x^mGhl;V0_B7<5Np2|8yjM=^RYIeJ@6XRnn2s z>OYSg=nh~k(Bo0z6|9$UAqIkbpkl(`29R%2y}C`k|-b5VLxM6oi(>t12b7PhyRJU1|r zOJ5c}|28zj+6v>5Cem_>vNJek)A?Pfj!q4YE7zx@;5)HO6+cdJHnH}ZY;`(|$X}qb z4zP|n1O#Hj9aUgj#M~wrNC%WBL`~&58Jn~6 z5|}{shElL-zb)^E)6_*U3cu>7vsU4nV{{tF+^MsM#FlAm<4(UeHe+enacFMa&ak#s z>!4-l77JQ$s+XT$TH^2E9A?Kk3JW+XAep_+C@ij{w7IR@{hQhk(-1ziwzNgYo_n>O zp?2dLN)x4b+21Ex`Sy`>UZsFpO}XKMZ_8B>Lm3V2LPbLxsXbPdO{Gr~8HI)6l(i6m zn*veorO%*uUp7oQ9)8>-HWW9(6w^bo>9Fw+ zj4}{x0ulJf*Uhaag)qm$y@mx;uZ`n@wqLl{!}{~Kfp^-vvvpw8;#0FBLDLKtrv zC}a!n4ilo*1vLQP?cIjqCpHvr5X~SVJRpll^HlzsvLIB>m|Q1ap)7e#m>T9cOac(n z8M8Zanv6yuq{_Hb;B72+n`V&x6nhwDA&z8qqlOW; zuqMBMOcVHPd+PB&WXzNC9MS^Cs|W$omH+Sgb2t9a!Sn6C$M_!)@!Z>2pjD^m1)f7X zgCzm=*m?CrDo~D5c%!@Y$O`T!Vt}YnUfwg+D$ZAEV46ai!9aZ;foI#mf3$xh?iq_9 zo*)8}I29mU1Q+1=^(D(WE+C+@C}U8odzS8LvJ(i&3YOG20Gk?mqK)#bslb@B1}jrv zlB~}EhX+hzMAyqnuw!HRbCPwnu2@OU{TxPa;3P>zpU_DphcUxP?{cW~F1|ip1AxeT z<(;9QWI1mHD8AD#0>!aEMW8#&twgtrt%M}?q7UU4{)R@y6w*@;!hD|>nBo|xhv4Gm z)U#?O4D(!-(g8Ddy(7MAs(uI3(INmFc#dkJt}D)gDhr&iTDJTed%5nVH=-|P8A zxSsc%Ra&&}y3)GU?RNo?#`!5fBRo6;yn^OQnKXTYw>c&-(O)YfyN)H-?Q_Yo>9?SY zjqL@s<)XT0&$QN3MJ(cMhgO;zK{LjVQs#6hbA&Kmj;uuA)K@{0es}!LNroJ z#OAE67LNEGlm=|~4UuHE$qhd^;Dn2_QQfn6!A%iDGk7BNN0@J{5B7twwS|n`X z!ep#xJdDTV1+3>vrw=uQ*W1kCy7}u)2(m<=+cIjmk2mJ9m_i{Dj(m9R@}y`ThB`MZ@IRi+kr|cl2DaQY0*&N-#HVmn&)iE>C=aPgj(0M zA#A^t;7B-sRq1b^-WQzW<2_gNf1@F?Al=qUk-WvhE3asjjmsqe7aD0ly!?%iwDrnM ztwm%zLXS7D|K>PD6c?*l26nCg_MY##`+uzX_t`0=)~|$B=Ha5X>(iWj}*N&ZruL_{1)9I*lH&NRzbEr2h_cZpWzG_ zHs=5^UYdDK8c#A8!Fhq^kkDwJyLhV)p98lT0pC%Y?^bY(m!AXY1g9y!IfDr*U`&nn zIv+jSS%vRun$^3zzbtL>FFi9a?LLD+4AiJKAyW^3Goc-q-Pz zs|0}q4*N8BNFC}MWA0sQ_HCbkTMpt5K+8Vn&{I7ZSo`7EEz@qI|1y<|G2m3 z=6`v1u=AMz@j;&Yd0YK998HN4SjSw~8268_ccL_(MQT?`2iR%YS6XK0cJT~!mLZ_F z=hI~^?Ha@=&9hT^=pX@GuMUT~KzK03D4)UNyhKzv=bLX<0H4QVJ}&7@Lf5}n0J|)% z;b{RUuz;DquIj#B9*8b zv^ae5LO0FScCO8<*aN&k^?Lw}Dk6e_p?~*>K{9mCLv=~hMg44mFdmP8h1nH<{f~G4 zClnVebO7qg|G2+j^Z(i3KX{z~AL8jb|DQ977&QCy8o#Q^-#U;HbBbzgRdV;=Oqr8i63VeQw9ztB9+rt@*kgPWN^0HBDb z@X$%(`5jE>B2X4v>p&70i0?Cq?1LvME)(!hwlK9$GmAc-hEeEO6r)`RiR_IP)9 zZS5)F0@|3Bm!m^)Sm5{y(nB4HLG1y}%1aqcVCRL1#K5nxAFMqE@eCJmqYx-Yhv5JG z@BjDz26=gzA~KVmyS7&K^zLI%{}MgDvp`ROW%Ts3UOl~Q_VmtUPyez#oxq%o4ndi- zGP;8k*6{_P(G*au`lMIy?wGy1?eE=Z%lGcH$KGx3-EH2x=ypS9QyCqC0?zR@1Z9>% z#;>Tj1D9!(U4ay3kbp^n=Q=ZMqLM`q*9OlGZLmLF#Ga`t-0&V5sBk_`;WbP-e37zB z#6vkP)AXDt=q0`6lkw*15Bao+5(wU53dz%_YioZ3KmGpo+tcsQE{@+_{Ji!j5&2fa zF~dZioV`>HnczZa)jmFb4K}WK!h>*c6oPXoa^F($(+{WLo*ln@{qEaJ4yK6C%1iDj zsrR{Si;n-G(dAJAqht=p_yJi*bHSRDxUAds|;z?>*mnHVWA&{sO#|-gbOF#Tu1OAz52{|Neb~CX+Sx z4;YW9h=QnsSS{1QrSI*UxR(?^-$~%k^Dvyec(K0?FToDC$k)c>u^-ynmtTI_0P)X% z2IHM)n+M>_ooAabz@Pstq~j2PFTkJ00l$_xCkkQn&Y{`bPSm&ZwTh59{D zPe0G;-P)J#w?pvL&ud?<3CfR?#Au<|k!B++N>iOwnXG{r&*wPPj)$Mt0O#F{XPeK# zmoL8Be9m}R(Zjd2lZ=S_ccOx1)d}C213%3$p@642fuxeQN*gj^OK?oKXhg2aRtD(} zF0Qs>xvvMu3;^LGKAzxWoa2OS@eCO3!?6I&n>v=54L0}U^4gd6iv9X01Q*d2B*4t4 zc?}EjzTQo26uy_q^lD#OYrMUun0bm5kfI5U@8T4KNQ`XuRlnrCXI=HxS1*{HfAwN_ z^DEX_0w?b%0<+{e!`a;&m*fY+2Y@Z#FU2`5hZm=pVV zLp}bTDQs<*Z8zCv`^(#9`;T^6qg}T1Oxa~L#Zxl+{h<(69rjC{`^hrJWfZd_|L1@I zKmQ*fG0yE=DN_CM0IMK}#T*fllY(Mxx9D?9+mI`uNH+$=z~Dhl2!}*r1}^VRJi|@E z1H%N|v+cRxAL_7YM$GDK)`fuS8Z@23qK6f?&8)aDz5s_Zi^3uJx`5FY(<$0QxtPHM za)q!5E}gFcWg+*Ph}>L`JG)BmQ#pC* zuy*ftY(yOj@cqtv$@p_fc`8by_$GKS0}8%p682t26nw8Q#z$GDQWy!W1PR3==iT=g zuh{yAv*W$aobi5>_hTj1V)kV#9wiQ31ts9U%#e<<>2V^JjUm73r)Uz5M`J$U58`}0%qbA znDvLW7Na>-v>bvUFjrut~>*CiRlbnoSbrL{Nd%PxXmn$KAz5suqhG?VdDwDZcv&k;i5NK zv1gNq@Z^zPB{=o_hnJ@uptyGL^v`j%W-}v_MR}egnD87dE{Qte5bqFjymL|nWNYCyzUpesqx%4m_W7raTCj_r4AUBA{GsuHY=P;(|8m1!JbV(q<6YyS! zL=&?HALz!LuMuB*yyrU6C}MsK|9Dw|tv?B@yBuqjBm}&do}?1oIUCeE>Fawo`KcTj z4CKk#OR(`5!vNIrl1=bWpE9+`W{LN*@<|#I!c^@$8OQ9+*Mi2UPvw@+)29`;rM|J! zO6sXF;gfU-B*HIkgl-DT&0!iB5vi#~yl*wUFH5SzALE>kaY@CdRFf)wugTMEouBlM zQi9`>=e%Kp(Yc+ZeWkagJ|3&+uDt%1?f_$l!1!}4)yLKsjBs6eT+601Zj*x%9}_%A zVgjj{{(#}vm$=K~S0YSQNnkOa@C-ioZgC~;| zW%h2A(0P{_r!dNt(o^(&eOG0rc2ZIak-Le+0F2VKY8DBBk$~?#8{L(!BC{Nn^5V4t zvvWMA8x!*iB#(nH(jRe?IVy_i&gLdxS({ChCUJ7e6`j(+FU1A+=s`^gj{G$Rc>%8> zGh0n~eBR=Uob$_uqheu1ag581j)k41A*&q(Vp>t5JG@0Y$l8!rHpnK3Qi7`?%(X$S z-$K1>gCuXH#UsqMCc6hwPL9`dQZta1qeb4g6Vs)b^myDSlmT2Yp`%#EQ#(F=4c=DC zT`FcofXCZb2{D04a=B()0oG%W53Pg!gUy|N@a4|IvmN%w@Uhjs_woO;_b<(jB-frG zzUQZKW&W>gbm>Zy`a1HwLo$_z#h`wN?A@qoSf z?L3RWd(mVU;@bR`@_gD-8Q393l7IJt{q>xQB+sPLi-PsySJZo$>*jp9@ z@s!rs-p)f=W3i>b0c&jM;11C)JDzc~cdMlm7I(v2>94@<{`a`j`);L+fC#(DMj``e zlvgUrTx(8)CVfxe9L?ZD7`9=DI8!1@t+-*jD{Yt#G+_RZ#gmPYm9cKnFjnb+RGetlgQe>N86Xc8L#Fg&$G_xdt=0~Qht*da^A1gXKTOd7qW zyzZ8kkzg6mMJ#6$R5YBieq+l?VKBctouG83?k>AhSLZ%U8EyZW(bhBC&Oxi+S@%lq z2Avg{;;w&-?k>AUpBML07q`1h-s=JI5kr>+6##U?Tx2tu2sV84nicuD<^AAogR-w~ zrA^Rz{QGZrB*wk&ZV=E#c>j>J12nmtpF0P4FmL#ZFIg$Y7nnpfW+KZ}hR#mnwQ0}G3{p45?s8smGfu_H(G`oj8&bv1A7&(ICv{PPBjdT5Rob^zZg|_5~ir;@+*Drl`y2c9w%y0Xx%GQ$r$wEUM0HUtB}V= zloI+{=VAuHh~&4grQaysxw6YcEd!XMm8RAtYZ9haq5#oRp^fv0qf<7Tn_C2(D@G)P zw0}bk6hQR|!3TX(Y!F%|#_|hGGm)avhXNx_Bi0xT!_dv52PlV}#sUwROklOJi-kV1 zgPNXyI2sl`d^$O{<2p@wl8DT(Sw1rS)>d^TLCxzy2VB!-P8 zg5tA@H6wMlcdz*w|MFuhk|UKQb-ejjOYbf8$S4*idMIcI=_$TosZw#7tm7yacO;8; z1rz{f7vj3uTym0B&4Hv-88!r)gf0tb6I^;*Xn|!lWzG_;C<(UsnVKigX(d>yq9)Tq zu($ts_+j||?P)Nx?5kR+w4xNcGW8>oUkS6Wj%EsFs{Y^G7+lT z49}}fjFuT;PQjv)8*d*mnon+*9`1C3u3|KjEwg>gt z6j#j+9ve%#=dmeG_E+h#AqrRb*!V;GDm^xfPK+BoHmmz7{Ojcm-=K^D*htYFp$tUX z{Ic_`q5mQpdS}mR=n$jJFlpxlPbq8yCOo zher`Rhh()IAGG#MqxxRNW-YDhil?#&d|~GLI2<0cOpFajcfjxKpQzpgov2m3lG{&V zO554yJ3C?&1Y}dYw<)c&z1=)u4fxsZgEz?*9zuOZMo}!Tc(!=`VVT~NAHOcJgV=GSd=)eD`s%!S>43D>sfiOdPXY|C~`ABrIuPu7VUv7VhNWuFM0yIUC|$coco zl|4zP2vR<#Jc(ivkvC=6xt~^T-pE`4teDJaB9oz4fBN?By1i0=j~QwTX4jw069K7) z5DILB>bhYu5|JTJo%oSm7%xie$=0P9^}^!No?pt_A8>_M>%(0*(QoeBA5CyxR5vIh zE}v+|RhmPQ)m9$sEuRQq&w~I_N*ci6=U%sq`Nr!f4cru94G-LqG+qpigQV5s^L(1X zzdyGPDeaeDqb=7cpG-0_;kh_dm?m!x-lS)FPHCH*40^vgYS@#s7}xfgpzc=>X;qZ?Yra?G1v^2An?X_L?|AV?D=KO zB;@;|^l7HJwv!c&l(X|?R-f*y&gw(`mQ0io1vv{-ib`20ru>+FI2zjMmfK^>eUn3f znX1>lzpB@LR`r&udR-SpiK{ zouIpl*`WUv3eo;9A8mKHgMc)ndn-i!J~2SNmyPtY_PuNK<#0otwo4stU-nwLYF+l*FB5Uqy6m^Gt5%|N z!xUO}3BdaRYjZM>#VI^wt<65L3A*^^iRNQrh8mA}c!2>bxUy&OPu&hx;HMkM(gU7Ws~jQ)|Z2DXBGUWLJ8#2rPL~uLbq$$+VYrN<%Bfw3Wkgw zSFNU4HFE`9aYTGcMJ7Q=jBV1Z-Y9)Mz#z=-E(BqA_i>|CgD@e}>aCGnD3jGo5UNN` zUS1d^PhCxAIJFs16Kq<_-F`4{U96y)f0gCB6dz&?7R}IN=bPn z8uH}Y>qlxk>f5%2+y7_pPhFnl$D&YyO_eEx7?8<$#%IDDVe~UJQHpUn@D_WAGQNuh zL?)SFg(D#-c`lXTAPZT#?oIz9#%~p(KY2>dKFzZNe;Szchp*owu@d#>~SX;bD)RuW?^5EPFYRsJBc6UR@$xDgt+&Mc}0( zFnZl`q4P~!g>t~&D;~XuqaiYun3I7ao0yDWc7MPHP;zpNc!>NLn6=@L7kG}`DW(?C zO92Q`q(r%Giw#(*r-Up8Wi<+G`4)=qmT|w$GL?}QO2#lYHuG2r`arNIXc}PWZ)8@I z`y-xnl*wl)yHxRfCiJbYmBT-svbRUQ(jeD!qI4d{iL!502DzDuL|yXSOc3I4VRM?L zBBR`Em9Re^zW)RfMeNtCI6x~X9lfVeI>O!EUT3!-1f+f3AAzO^N=LVcMwbdwWHwK7 zITLV^euTNQ zKEI_yb0c16sFF~mxo&F=E^gC7YQKat7s+g^tZYw(ci!mMvB#6JKB@nqij2mT%~6_t zJ&z_Lzmb^YzVt3Zp2{{x?6llGw^J3hBG-0n1Z}r2yX~!wTVuCyi|x@K+uI@8#hfXf zo8DoUc{n*GL{iuS5c|hC)AJF;GE;0QE&`K@4U|_A#Kg}>Loa&ykyZz`k~a##ZNbeJ znQm(r>BvX`V8KJ69Ti_1UR)AWf(>x{j0d-=-M5K5d92%E(P7K=*XH3&w8`MGPbSxc zUbBBUtOl#w9vQjZ$l57~Yp2Nw%dedA|0l3bAE`voV@MuD6c6X90^4;(q+B`fP=vj5 zXdpQ$s`rpHidFzD{8!C8nV6Wv1hy1&9!(n<8~~T8E-QkV6bNrXfLNYMacSN(dbRn| zynbwBnLkj;Itw-x6gJoi$a@H=`L=f5jT_Mtz@f(~%dk&oPs!$R_i&P=d9AzVX82iF?(N5UNDy z8J9_}7f7(9wMjb1L(p2y?t%F4CW>dF*)B4@+zl*v9*WigZby{XtaI24WNO+wFU^Te zKd42-J9Hnvc+q4psB`!gx;MX>?q8Gx2rN6nyk}hm)Prp5D2t>H)uqU;OEINb=JLgF z@#XJcG^s+?fPH@PTln?67yncjQcAt81Oc)}PKvdZLV)&AW%(ORb~RPyjmV7VW9Hhf z8)yrT3AmBlHd`2{4IWLrdJ~23nBzx+U(riFS9hl_%QX{UpqwW$=Sn}&vN!8hChx45 z)QX1fCSx%XI234Kq&eHi-2d(EzE2+Y4U`nN8bF^mS7_YjdI|i1QzjX*(I*yhPSBoR-xxb zeY{28YxNG?qFUny96WP>gTSz0%lr7l@yCC}Tcr{PQZ^Q0KrN5uB&kA7;@eAFE?f=- zctXtr|M4=jkB67LP

YNH+vMSSb1h6&MSF-Vfx-STVrb-L>Ia)+9p%z{qq|K4v`C z6q*ii&PR-=k`;(_Ws?z)qUzze&yNvHVgbtI_K!gFrbH^Wmfxd~d+Bd_YNp4(K+__zad7D}SzFck2?w zP8D9~39NCVFGjn390X*RyEnW}^MExl{Aq(3($Q-QrvjbfkgH~NYzRZjm8)j1H`vCS zHF8?4u0{@&f3cTT8JFc8w_zaFc`1+%Yo=ns6x_t+@6A@+#F)4OTayI0tpd{dsp%jl z;v)VK*V=Yj6T@8(Xk8{d6RFZNR|_=LnlGie=GZ^LEIt`I%O=xyf9rHwk=F-W;!akf z3}R=Cu}~#&FF`Aif0#m6xkX;}Lnm5B350`^wVTDK0{rwm zV2AAdx76Wx-~Sdr{_dQGYBo~n^4E%PPZ6=H$i#0j;+y9z1j%kHs1Nh_mHFJSE+H~G zmIC7s!Fx`FlbV&J6}D-f2zkPyOpZO33BsM@YcMAm(zgO&C%xTfZ;v(b$D(l`kpo;_ zE5v(}b+vesZDO1x*b+$nx{yi4zWtUEfI)_E0YhMYH!Es>s+r8epc7;QQVVsH6}EVQ zq|ZfWMpmH9v(0zI!}jmrzB$}t>MGGphujESc0WNg1J=DYr?yC`Z7qyd#SxoIZ8jzb zT;^ORgmIv*n4ts$jW&%=O}92Fz19PuV}mFw5pD=QD$T;ipA6cci}r6f>hLZ=E!D?l2O-R_X!Fhr1m8%IN`jT}=nlnr45 zQ}Xe!IE;ANwBbxX+)_dF_gJX~Or!I~#X2`nt*~15I=vBE;K^3)wvpk&oiATGRvz>_ zr4f!`<-G@!BW@Z8wdIZGN{ql7C>F=^Fg-$3w{}<=`zf?LV2cLlcV}nACNuw@-jt^F z8+(ro;MpZ}=p6u_PZthe8_W!~b5VVBaq;2hbG)bMS?8y%t!!hb*Q2GDlqJ2`*B3hR z%CmSvj$f&zD8lk*ZVpn*=uJFV88f#+$TKFBKO@A>R-N}6YC3K=yFJ!0Kf1M&7ybuV zEt1%dc+MS_pR|jZ^i4feZWa~%+*6*3AwJfS z;s14OE^(!RYpNpl=Hn?ruLSHcO|itpxoPtwmTN_a@Q%`Os z^0wjq10}D`?XXoP?Utt@-Wor?69s@m1^I)^`z@YX9k$A?yv1t*qDNdqioW^U{=UK@ zdpGEd_6s!D?E!v4f3IJUg}n$){p3N}EmlR6dTJPF58><=g_DMP`seg^;JcbR8_hHH zyW7B@8!2B#MYnUqbUa`eFj8pm_OVg`7tiPVFp4k+-FH8O3UoX~Eq@}6mYh;V31E3r zwR~(L;MfFU=p_*g=UFBaL`99Stw%}&gqHP2vCZS*W;K`q1UZJcke{P`q2`G9?x=W|Gq1J}GZI}TJjXu=6s_9W8`m%m&{ zGBMBk*;S8CU4b~C7Zq$d^YYA1`v7up)&_ILJy{A-7lF5XGan$fiYG^sHZoLh~U&i4M!-tO|j-oNEyea$%UFv6u<`GNFlF8lg9`x4VpldW;9qC;+W?_0qqI8KMUXW7|aKp*FTWi&^Xmqix zGn|T#!y>EBlU&B;NE8^#xdH%bkh&;eQRtk*Z6%_BeM)2=3$6X=U60(|&f@5wwDV%J zjBZcoS6)BDpL<8|1#gzLaA3HV ztPL06oaT92JUn18uEYo(2s=Bw%{|t@AK!n^Vm0wVL_?94DrS3uP`+U(ElRRbs?uCL z7b@l}H(jDT@Ct#z6)sT~v3G)^p9{*LY?V4*1X47uoxMNxH5O~xG7IUYAG0(QS^#Dy zLD`b*l!*z?q8Kc5W9N-UkX)HFNWs!UwJ?rzD*s{xxw*2QuhR>e{!wIdTo>qCx1lCk z8kS9=%it6tx(+HT*){($70H>1#SG-JBNbCkRb2VW$?ypG*Ipm?7yNkMUw>cwtHiJh zpLtQdi|2VJNAuie6=;FH3|$p*$=hMyI&?3&P?w$k9oS{?=$QYaC|FSuIcH&*JH4s?>co zr1>i(5L}+#CoAcd#cUfT>SvWa$v8}3Ez=o-b=$6iBbxKH+ut_l34bh{r)S>%cfh;X zlnn~bs@Iedc6Yk4*WkzVUi*98YfmO8?C$PBWFP(f{(GBy3VW@{KlOt=KyPALp3&M9 z5L(40u3A5RpBddV=3S1eJuCc$Ya{_Nt}QVi~VWX0eN2L%~^eF2+2E(#LlD*v`Iv(FwZE zPS9_5gI;qd=)ACjRPb(bJMV)|g+D5Lm@VRQ(5ErcjM!$ZCTP5M1Amh_yHd$FIU3BE z9J67Ot^K17PCq(%gN0=<)*OMzZsSyUZ5qZvM!Lh{F`$2~xFodBA5;^0aIW1-Z!nq+*2@Tv>T`8nuxcA6~cbj?3K_|^Pl{)E4FnylMoJ@eE2 z+M!Nu#gf;qJ7?qHLuGx>D(hAD^(^lFPcH7Qsan|cv7TD7-|3lMhCiNn+27wTdorbB zzu$qK20y<4-jzXSt0xSFJk94>y<5~tkS^n*$#@*If1V3-|Jq^&)X;$beLpyVft z$&I`4O0Fnq#JmL~)qE`eU>+Nqr(D!QaWXQMs>~L|E@cX2JQPh^Bg(8h#SC=_8>bs| zN+rrWUjETIRy^N&gdSi>0I)z$zq2$JCp@pS$YGq#VxIDEY?SJtTQ8*DT9jrzGaYT3 z_RG>kEdu++eYS)9Z0GL#?3wZX9WcJ9xn6hoU^Bvx=gs)M8EZD<2F3hf#i)ZWurK^k zDzv=vjv>}cn>z&viv7;MAA9pj<4}ew%L&(EG2^ZFA!yTndOHvaE1}j@dUt3bP{7jx z=~OXmY(QYH%oS$-*Tl#|+vH5}#4N}+8t!nBAlM_^|JQ1sA#n2;!oeUA$bNo2)B*eV zYR+c-nqBcE$0g`@o84X3pr4g$*TuOr(~@Q?SD}j8=GoD({proHh@yng2kh)z|VNxB-^EkR*W;C_e?)mVl*ILTu!i;nib`uv2JV$ zuLnM-d!4&Nz>yL<`6ly&HlhfE(HeR%u&dI;4W zixT1o`#a6<4r|cQ%2{Kep79V87=DBVhSGnl?amFSz=-rPmY3oK|3Z1rda$&uTjsUg zoS|=U7t$6SBf$6INErT_uz4EGrRt6V96>c~jS5(*Z40oB z7Nfw2lSLpsSL&SU`ACPEOsO&wgLD)_z#TdE{e@+l0bnIkL=v*M%h0>@>N5v!eD488 zTHBv5Gp2zFPk#T~qKgm`}AhWHs6_!wgT!v8VT3jK0H(N zB*M)lH5CCz?`AaTaZ#OhaSA3fpUy`?sAk@M=l?3To(tXH-s^22yo5hOH8V5Y+S%Vb z*zN3Xck8OvE?Y-Ljw==nHIU`2<|#{0b{O1}Zh5^wz?%9i`MH?lmoT@*5FTPlwzdZT zUND=)a2wnvc5m>??x5p(Lp|X|^bRu^EMln&z+a<#nJ;M*@OeH}h8q{kwtX_Xn-Div z*%i->xItk~Ro{O$*Lm@jckjD8G;H%9Tfegpv|bTtXScJ9Rld#7y+t-xRyYNf8nPlC z_2ZBgEJCvv=v?T}M)`=2huX)WC=iINVacbc#3ULfo5B=yxd*RB8`g-S&jN)=x0HEXe+jbc}-ox>xQ=7_Vz#22aIcl$Je zb~RZv2WZ!soS;04)e?9D<%Ar#P=OeYI-=>y(z0rW2NWg{b=+FOp*oc5|NMjeA|flz zynjtpo;x4dgy$kC+VKMmfdw?3PDW+%^DAY?+62gSD+Y5Y9E5*=dNV8nx$j9EH1m*j zF~&5IRM`Yx1s=cb4ptb9u*z&cb!{3s*%}SK+B{*b=8;!7*7^)lJ6T$or`z4`HV@3M z-R>dG8K38hN(`c`^aFM0u<{{+ zw6j=Ui41iqC(9PVm9|bIpvwYYB4Df)q=h_z!ioZF@qupX7vAi$RQWYZKzsS6sIZ0i zZ0W}}L}WLCM$#^mK2OsdNQYvWRKPRA1eM&z@fHL4jrlfFIXp1q0PUYodBQ#v^e)We zFG&g-X74nWcF$Y+!a=k_Xfqu@I?HAdwBS_nNwJW};XDQw0?c2_3{VY$6cz&H?WXz4 zLX?-k;$fz=hA3N00kEYJA`K#6cXAD-J9c~f9Vo=r?d^A)T{!RKc^p^y{|4J&FH4~a zB_l_$Le5ERIv=$+*mOQxnTu4)-5jHR#U~?Rf0FrB&2zuxFR5%2e8t zt{yt}0Gc)hH`m8qeD^Js6z>L|mf&^O+n`nyTT*-2Jn#6{szrG|@)`IS&Jk+!oQmCx zYxWH^{*AqbIJ@Mrj94r~k!UJ}KrHn*ceb#@;jt}80$&zI5}daLwh`ntz!`&*3mSSa z-uO@yWP8yTdDzAW^owedC)|my;v+e;eAw0E+<&qq&uVMkqg^jeMG|Rt-m(v!yUGt7 zfhG&aI2G6ec91J}?#g7>@5}Rmo!E6R7i%^R-@FqnJxez28q;C4qwzSF$qHqn>JsmK zI^Df*3#pjy3w+j6n#w}il0v4zGAL6K8*T&j<-LM3W^O#lSFlYMreg5=IAIc>sXB;k z;>!T!A7DuRZV%l7{a(lX^8NQ~u?)lG!gPWgeGTM-!(;a0Xy{Hzu^pBluN&|T-tjD| zTvt_p;YdY*n4OrL!OMY}SPBkp)(lHyE)#2yGF%_?X3WyPdIiqLSFgZu?=%Ki0r{MK zkLTcgl&`lI^G&{HT2U8wM!=QpX_T&IkaXppw#4&x_^N1qknO-0aGOJw=y`m^a~`Y7 z+awoRnn^9lTHWs-H1`ZY+NNy6HQ5mvXsCMFVlv?yo^uuo(=7>uE-#A+jhZEM0H25? zM??&I4Qnt*o?DdX9KWkeBIFW^T;M9BcR5zzwD}XTdlVaE6<#c%geX9Is)>QHiypcU zH!@pq%670KK0YJYFrVjB&!b~6QLbgk4(IulrCjSPl|{Jr+r8~(&q&AH{T@ljcUpaG zFMWRsmuBH@DM`_&wv$AF{yuWF$=J^gtxBH-Xc8m%Ad%^_!XMLOPnAQrFRRN^U(0%&x7>fWd8*jNGTvc0ewftSCRDN_=^7i3>UfW)YhGjdUE?B-a z4jcLeWflDT2}U``BEdCFT#A~y;nf?KuGi_5$(ivl$5AYfRFdeS$e{H318s9~EcZ%B zkxE4PXR&b`HMdUht%qSXLT;F~nhj5YWTxp}WviFIy2B>_fHy3hVZ(pG4-Ur2^FQLB zXYWtDz4K=2@BaB#WehW!(D#cQy|fGO&=K(To$gMv+hL9Fj!`f=cjoDJyn!Cs0PlAs z^G=K1N2Xb7dlPEN$t68NAM3bY4)LPeiW` zZt$Ob+uRTESf@i<5B8?+mR7@gOOk>E5x_ZwAe|&cTpc=CELZ+8UpRT5TBsDaG~()9 z8$}+g{>-(YdL5;^8~byORC>U@xD8;5-JR|IJIj_9DZ)P^pxiHRP_jooBv?Zgvo5H{ zn=Bp=Fci_g7GE%IeoQ5Xe2CsO=DFDwb%&(ph}@|uxLK1ZUd3^W=i0h2H_p$`H#Rn2 zzGU0opcC{M`_<>eG<74dMZU6LCm+#c`n2DW%u6!^yRn3tG}KJt@%WJ zi!J;*RI$pwrMhOnj@3kcyTyL>xqxdJuX=y)gMnHlZMJ)0N2F)r_CD&pDJ#>T5xCuDhwcrFI7Ua?-Mx7+IMwK}^S z8?V?{e%TDONN)|yX<;*|!KejRSZD0}3De?WLtH>DAd1p84G5lZMj|yWV5wo3(-Eog z!l-2&4m2)E{Tq{MnE6qGo^f%pm0ky$38yL(n`Bqp8nA!+m;ds=*h0UwtIP&_>B>Q( zZX8Z|G7);i?7&El#~bD!)@n^;&bT80p4GuMpT%rc_vwaABJqXoba;2ScMt@kFGjn3 z%(|VENKw~NBVsXbL-o(@1@wTHN(YyuJ#p2ZTm41j}W!}^nt23 zL~Ro>#S;`Mc}h>zgi~Aykv3T*Vv&m`)i}B-qQb>##0&A4Padk<%Hty0p(B;v}cgOA}YbV|su)%0P889)EIV%XIs)-+eIR5yLl>+^swmWYh zMdwhni86y6u)$2s)BwHzmRHUAM6^J!(29Nh@$Bs>n}NO50*jMl6I(K3>(;LZjK^}4 zFnyim{0m(%mO*a5?C$UHZP5$ZieZ>r1E$PqbSlEYg`t2JlX<}x01ZF~f|PATxlVa@ z5t~htiI{(3GybJzb@9oZXAu@{&LC4^CKHvlQ!|Vv9!N-aHsA0+tISAjFh<*tGtUQO z84EK$eI?Btgeu7i1(hchJ5)Zc9%01cnVRPC(UFI&rM8sz$0P)FdT<=m-RUN zTW@$$mjSdO7mIk-f-c+XJdWFDmx4O>ik%zc0;ny;<8#DEG)Dq9mHG1A+I##51Bm)O zXaD-Y{%81t{@yAzilbj>(?!pgTc*-e$U>-P_o)5YcJMnH13*_2v9aM&IAvzl%#?XPQde{^}b zU7qKI=K=3sX7d6|O*^c5^S>~G6F*yj0P{(8OzyaGz5j~Qnuh_cP|7cb1> zZhXf+|H^RUf7%n~FFU8AaC$T|J-dW!IcgKt06kpD7#R%jdZ=S5wI2 z_!9f_D8^mcX;)p`q^>;$~d53x=#9&CJ9WdS~MfG-_iQoQweZn)}rEJMj-iq!dZatt>f#-$QdzyzjB08Q}t z8f_Tviay^C0#a@7tI_tsT---n@dh)*hp{=>5TAr`?*P%Aa;+f%CRfay!`N(z4Yq-k zv`(%AWt!ov;S^7m&Y4|U^Njs{Kvz^C)sk0>^~d+@Ujuu|Ig8p2T{_bljP~61d5-bT z3u}H(7LoGz@8%=6xg<;u_1t1uw_BEP9BYMO8tPJVYYaE*lU~g1EyP=G)(_?_M23JJ z=ilhUS7=2mTMxR)#HC1B3YC3Dg!itW;@Vl-d+DmfJplyD)FXy85IPTPTbFOL>gC+y9h)PfwicOI=!ZzOx58JSC-aj@VQhQTrNeW#`=NJVmw94J9KUUa(Cc*Vx{6{vNb3M&u0X zVlDqnTuQSQ-(eT;7b)PRGmQBNr1_E~|@hRhr4sT^)DqF-bHjnV-A@>ldft?V1Dw^LwH+jbW= z&34!2UsrYwnOb8rCtipz79VkE-o_T2O@gW~Tx8zn#%a|7`!LVvu&ujcDl{mcpCR zzrm_~H4A6A$SEs`N39W$c@m0@p|)rEg?-mfTS3GpjnfA_g1WTL!oGRuiNbQcUfbnw*$X?|GjtM|ES{m zOh7IMg?+tbEtpCt(?ia%`JAy?k+V@Puf4%G;6XS;q=XMj*l#)eY_pG_uq3%(j2x(0 zr5yR43Xs!S{X7a-V{57)5#jjb?|uxti8+kn4CRKAStA*|gEpVDlf$F8P4?#P>rX#4 z;pHa#)8Wa-CVP8w^5aR9{qFGW@I4w`hKC=IkEm^EV!&RQH(o3b7MsOZp;2fPo5U8O zvC=@HDUb3q#jY|*S!S@bps;c*JTGI>Bos>F>atU*5~9(o*8bv#vYcoNgJolI4$OQ* zW=UjIVyKpt%HTYzd7jR5>LgJ|=8IA+J=QrYGy5AO_{-&DR06{dz%YvXimx*Trg^ZE zCPGA3rzidwY%~h*BTSmx=udDYwl*5Enhc6W=C9yXHXiXzHda58jYm6=jYm9+V}_Xlge=a%4a4@vI<;jKZEers|DpB?z|)+G+G_hPl9 zkM^;<95P@JYF>aL;si$y56heXDlXqMD|luFe~YZ36#9CfipJF5lA6Yix?}%90zspO zC;0t6yf8qm`=UVO$xeU6JNu2tIr)vsxxcxahd%Xl52zU7+K@H{{ikTgS!<3vTh~6Wej!-|Jd)!9yg&qxZkNpKaGO zTvk0&ZHl^$)ucRIqxpC&Oyj5W=cicc%1o8IhY2fvvQol-F_>J=#Lp^ul5v>6TBb7% zWZSMwMq?lfT7=9qhj=7nbw!5k90cIifdM_jy=dts z7D<>%^t1a+0s@8*OWxH{MAt1uU{fZZ1!@!#%Xl)OuDl*9p(LjNQs^~Ma3MsxKmbNh z20V#a__Zo!gsq zBSP%u{;-FbrQRb-3+1Dj?0C8)|9hCw7Qj!OdQIb6> zggvJ8L0#3$A2z6EEeWI=h4QE}h;`i-`C`5i`BCH$XNC1L&X$e+kz^6-#>-!;Y@2(? zBhW~lM6#^)Rb&!`Mp}?cXn&zxVjX9tP`fo`75BWCDR&QfrQH3^GK+h*xW{dPfR$Xz z&HYlcOZ^<(S$bJM?ga^^JpKolVd@9=KvK*Dh1lI+lKESR>8UXFzn{>aI}ZGBG{VOp z?bj6Ov-&DLJNa*>;n~UmsPR5)vx~1a>Sv9C{PMv+&+OruJv_$%J$($&eZ+;w3I$q+ z7d!_FJO>IqK}68qgo3XqF6cppfyWFFdOrNmhyQ<)C?Qw6aHEox@0WAF-Sq2$wRVF3 z5`fL^djo84KLczo1=#HDTYyahZ3JvhgpEvO?13}N#Qf26wYCU=!vl)^a%~#ORCpos zMX**}iYl&T87I=d^Uo2=qQw^TMODCTJAk@b+lKRfEn0d#i){t6avAXj(ySZ7 z(a9SRq0$GYGT)dZb=eL2-Jo+TxH#;6_Y@&t)=poa{n{g?@7@XJ7FZy|3o5aRkNHTT ze(_?Ek1b;6<72?>@^Lsi*bM>#gS$5d+#Z0Mn;%#*qzaeGBMfCxy7!imy3aC_!)l()Wh7p`LNM3k55pf% z&JI7G{S)A_7U<5l9Qs&hfXs;>S{2jsBEuywyTMM-1+f6jYi5FLm9VSnwUPd(T(gmY zpbP-wH4S5xeRMc}e%bBr9u(tV4aiw)Jr}y&-Q7Ld3fRVX7^weg_`}KJo8yl^tl8YZ z%lYWrOz>zXTI#Yjma%v_hv|~Z1TlNFITb}S%+`TYe@2vC#3Gl`?U7DqH#%tD%YOUi zWq+@;6)cxwcUs*($*=>O!qV)UX`awXF*}Vm_ZWjcyZnqxx5D+2X6405MSaPiEr9g-XC^^p(w$tfj$Wrc**ArC~ z@?!jw%h)hXzpGiAUl)A6Y>Te2~W8k2?P0&FGro+W^uCQ3X`4F?!B zE)y_<`I3ZaJdG*jkQ5K+K>H0y9BHI$CA4caPdDwmehuD-KY2fzkmXKFHu zg?93dElB^?Q*hYPlqZSsOulThG#HZLLVQe@cfZxQU!t5%#`{8V7Ze2SQ*d@PLtDWn_OIjTVaBU~^><67?Ho-HE({T6JE_7%d8 z(K<}^zTw`9E)8i~0EWM-c;MQ`yvK!QU8!ETf2Ej4t-}wv&lh-aVi~pmiYQHO6tQ4+#y5B$t`1*Jle5T%(_sL;%e)F)=FUlxI8y zTc6>~=8?1O6pSWAsFY#u?6+Tdn6u5uF9W<~t-zTK^FQ)fn)5cK*U@6P%4Adq1Q(S# z(?dArWb`+i$jr4fb7QQN;Q+w(JJ&Jl~+pZ_w_> z+7_tp3Kl4@>SbqX)ywCleaxjTYH1h6)0T=gC@>jy0E)^dMc@x@4=lw^`6!UN=86T3 z0d3#NA<2Y^FA^)mS!LU9FR%c^PWeCnZ&xA6ZZfK4;GD~A%S)YB78XJidDRWY{`Nh> z${WYInBDGwN7sAr0G#UH0r}Ve`rn{1y%uaXk8>HubDfKfyqKPW;OzY=Rz#9Bq2@M> zNH4Z#C*k8f=ZXBnhMCkec4VYS9=j{H{NBI)%m4QOvKdcHBzw$tuVDsIE{at%QHEQ8 zsgN;=)riN3^L+X#iwp5`>GOCgR33C&uJUl2iBLpFqg;FeB0pf4(Zg!Iq~u8;Q?M)< z1=!Z8whdtcDQw8Eg9Y0Ze{GGp77{Fb?dZq82;=inZ9jmWjXWx60a%xcG3+z; z#Z7ij?OT?24n@pHVxbBQJx3t+&js3+)$}7;sWhve^4`wwfRDl;2+p?x7e3j9%yM1g z8kc7Nay~f^z`4G-G(|4A%$cT`M05FkqwT<3cAv0VO=QR}60lNxGqWkp%$f_yEOR6> zd102?&M)v#V0t+1}sT+a+3I5)CyaA}gP+McvX>)u~FfSZ!G!Z#^-d z-O{pTj`q6;z0w3NuH(Yz>n~yA3J@Z(x>lLbqc+a>~{R29AUd>D5aLiQvL4E zgBgkZN{EEzSBjCYCNCj`C1OTmK&gLC%1;E3YRN8r@z6A5b(cF6eJYZAqDx<_f9z#K zZXY8LXxPHYjPz8^LB(vX;-K&~XaSzW=)Or-I(M@ZkFBERdi3}i`vqH4ZgeY!l_bCvU$iI}G##L<~U z;2dOCFoCzOZ)Yj(b<14)z+Je!S!*ymmf(hHwKJg&o0AC(c^q5jYyB2>RSLH}PZ(~6 zluyXkhf$%XhqvaNsw2-gQ_9G!JE^&bkzNRqws>rgOTo54gm)fmQkPdhJ@(Dt3z0(8 z)#+XSz#1$7s?OS~e=tAf0QH@bAhjMrJjMr}>LxoCVJ33DWrkoBH}guV%4Yq23$ECs zOrcW>9zC(WtbO}hXR!6}x+}~kwaNs9Vi1SX2V%eR4KL6jnE# zU1}Klk3lUV$x}l*nMiUOqAwqeMR5!-#45BrbSN_C5-@MH=2`5A08II!7T{%V1tq%C zbWwrz^mGbCD_IRdyMmI+a2&*?$gT;>$*5Y#LuY4TGZ!7=0Bx3{pE6csp(y857B!Ib zY_4-mApCO8rYePN|wHQV$3^ZN&2T7f(y!~=Az@_ zDQqym=i}k!F4OZ-B61Wet=2us)I7znU6?)+Nwp9vHkHNUoWBGXjl{z+;_GBb?t^5)}zifG01{^?j?xpr%jhk-%*ioVsQYl^3-XRr>W!dN(S{=ru6c*&2pwl1>| zyf@#M``l=))~mGu&>^u}KU!>ZJ|V>Nd$;3!WD%|jStHr#x?&z`Qrwqso>*d1*nawQprk;B&9fL=Nf4dpf0GGyj6Ym~ZtR-5Y>pW z=cu!tqig6wT(fT=L;V{I+rZ6eWyD;ny7@N1c8+rwMtV3rwvo2z6ETmKF!rIQJBnKn zZw7hr8hQtr6F&G*6m);l7J1ml2lNX^_!}#?8i~i%`%grErLqfO%?2{mcpS@QjgT+D zHnZ%co?`ZqjNwIfeRMiX667mcITc0KIAFOMpDP5HPG;Q zmYKPAu|l{r_v#r`@BR#`_spP{FsOd3-?;;W>aBV`(aILL&~E)A-#4h*H1WL1OBcE4 z7TF62n^Rh=U1+nqq>Rvv)~v33yS&1?#R~7#ukd{%!%P#;D~#E!7}_=ENfe8648)8> z$AhH`VrWoaFwdU5 zsMB-g?+N*?Ovi4w-MYy-euni&l?(mq)y4+s;trw&Q_kjEWC%DRG#WzYX{cs~w6juV zPq49ZYV)f0f{sxy(L({7IAYhhHp+MsPT5F|RVJ8Ak`CO#*{(e}2lqZV2hYc5DP!#H+(n~1 zxFI(K`QdiK5Btx{`pYe=%lt<{7DaIDkpUOY`P$NtS^Y?bdQ*Jivowat#Kdd|$QvTl z|M8IElpGMrSY@GzR=-mFkFTih>~^+y_Ig1;8r!`UwJv1(H{d5kE16keFR+l34-B(! zjX6LpWhxdLzqLE5Y~3`g;pJBoDh{9L(**wgxozlZzw{byxkmY9k_mI$ohk5BK;)ox zjd&qZ zrPbC4Wea4lmaXmIo2~6Xv$Z8`tw&<~>h{1MajJn~g)F~Ll~KWoZRskq9C1EFMf*Ym zx)gz0+7P8nuqt*6RU+l#1%wWxL+<$X2NuR^9#I};8HX;G31If68OvoJKLNm;=!!`6 z`$0gA?%w>Zf3V%$VGZ~R*>WnBKg~qWooSy!sU1?zd}P0Y&{`A`HyF8`oWIC05Hov6 zFnb?(b|LaK=Ai(zOhU>D%v;CFpG8PWqQx6WJm;~Rpx-GHX{;c!*PQj@p;KVNxeNhr z$Ozal=OiBc7_}hw1{)1UfrCF?+Eczl%H)P0cwt&I2Y@7LM7Dyixb^h&nCbb5x-3%T z0br}z7n!<#!$Otld;n>nCAL(TarVrX9+xd4&XU>KnsQk(0f8WB2Mo=MF`h~`xx%Nc zikYS1*1(EJY&OqPqk_l*h|IGIH!cm<%eawWTmlkupIi1;^_>VUlrHB9bmn;+mp;su z72w7)7SseLaQkU3=J;A@3P-DH5OZxSc|t2on)}`xvAQ-ozMsy5NUNq#(|@WW@R4gP zo+9ByX(V)*NkmQn-4rmq%yTSMGt;uZRjKJdL7K}x7vc6^5D@FWFUQ^i$H7h?o!rGV z4xH8B;XM}98L%G{0j3Nsato6HYj%kwg^P;e-u~FECT{8`V0;_5Zq;C{B9TE2$qL@KTyE;l341- zy0O#^-&lYlVhH?MYQpiz0-=mlEX_7$AUwUaX;W^ROypq@Y;82EHVN#;OoboRRl<@* zEYX`GLtE4NsJ-#9k#uMy-S0R1U?c6L52#`zwLP+xt)&f`)w@{~8~3y*)&U*u27C9m zDDFL56qi^OyRDu>fz;%K+WOU^#m(l!0`Ddpo7ukhT)-?hEMk(%a&21qw}1KH|1VbH zIBIrohId{y(vimBsyFr#r#TH(T8_Yhm*<%%+4_F9im`963ae!8qPSa2@J7EXtbi4? zpe_aig8>lDJcjy9B_lRZMw#Fjpx4JX!#IaO)jUt<;PCw8;rmZ-PlH*Vqk6fcsZ$9& zHzZ_Y%*~DN#)EcX+f~BeIIhQj&E9Y^Q;8P&l5ijkE!}4@2mq5n%cF}bB|--q8{L4t zdgV>bt5<_^i3U4)uU@GnzJB$pZWbH%lECoWR~QO1$fk(BdWA7PuU-LfIs&%y zWMvhIH~`NnGPV&~FwMlJRC5+Teoz@C|093hEvrUd|$pl==Qgo?DKli*_Ns{v6Gfz zC+(L4N8EA=bS-BM+}hgcn@JghvGvHmdCjaq3b$oPiMq1#py3m-3}tRPP9hLjEf$v| zCIH(Lw%(ki^EhsIcRPLB2Y6DXW}N+m+wv#YW(`dq8J_rlfnZ}};4$l; zP-q#D+YoR3B6Sxklh~-1>KP=lS>_N7s3B;4CU`>86AQ%%5|uM)D4T&q8$3bnv0c75 z?)8FzJP!AE4R`jMT_g6GpW8LghKc$hW`I>N6j^9?r;&U*!GR0$_&ih*dn0u=M@{1O zJer8S*uOW%ym*N3Je6$(7;3o;_janHR^-}ljiBw;Ww*U$gsW=S^H1LJeu7w#f-WR$ z9aXEMz%j;V{L81r6$ZEgXsMi1_nVnaay#3PtElaJnJO@en8=#?8GA+44VqMIc^B@3& zv8eIfaJbKwL(r$UQNCj+@9dT+zwEZQ+b?C3XDXUQWEd?@i=MI$w$XJ-=RfyFz;j!g z4AIXHrm?pKY{suajBs@t%#%%{T45?WT1(!$*l#sUc`ipXHg|kBkA)_c z@Bmyjy-pYRBzVRi*F)4Q>TeWC;J;?mWo2NDXh}jL7Mc0R+-2w)k1%A9;_l3p#fnFo z9i13q77#cKAe^7QVdIn$1u#z&p3n1%=?%oAMPOnRL z+l4MXa~q)gXKV&JWM-=rz>j70cE1rqtTykQ%ni>sy8}@SHqYDco%1dBAOBOzQEm&{*rV4X9=-S|*al@14Js8oa6u>j(`>fI5KB%vF2wjv9Nf=#b%kN501+N%;EfUsCT2a(6$LUMm&>3ncsLyXe9*B2pZCdBf(+qg@MZ zBHqGLn2eDs7GnB9&n*)W)KOKc)7dq%t9qjmCe{UZaiSX z|Av6^(3*8x*^ZCUQ=J+F6!3gnc0;goT@(4Ui0qbUn*hx2>KAKa)M)Yn8DW8MS?pr2 z36RTGs}`9(Kx*v9VFQ|A2YPF)c`o%>3XQ=w`@7~Q+CEqewsDTX&$*n5pH*^uDNih8 z%u!HmmueWBHd>|;P1lZXbYYL??6Wh-lzP;Dmp61(m~(E%{(?^N;k81|O0?p{AF0T! zTCNbf2K`G#d0@;lqr%#VpI?RoB(sf9MJUJSO3HXLah>6#@T6k|J0Np6b~EaU zWs@(2Si2sy`L^QJgad31ja1{>jyR|=Evo{uhpmuZyWX}odMlp_s*^2~FrG)|Giu+J z%BH^ug`3nZrQIY@_#LAJ!68DgRBp7dWk8Qu`y2kmI@uVi1GV(_tQw#~oesdr1s=-~ zvWsAD)*3pw_}l8zuw`-8UO~yZLs&ow7mnVCRd!;pv#GhuoL&zojP;nF)a8f;K-}Nk z+r@+E`Np+KqQgWb*E2QOpETG7%k-4eL=Jy8%JR|_5qQUU|Hgu6NidUPrnDO8U{DrG zYp&ZyOiD!7LZq$stZ1?Yi66SnO!=GqzASbE=r!@1yqfX_(VcK-lEN)GnW zYhCP1_(1V_IX(4pjn?E^Vs>w#Y(djy-+WewkmVqLrvf7*dG6nLmdr zyB!zyH>@!J!q&Zhx0B9{t1X=LA&C`$8C> zD|$pmZ|zJx#T@*GX(6DL$;OJ)Tel@$S9NlDBFWn0o*H-o!>NL7WrBtFCEKh77{8)x z2xje|(`k11S>vE{(CqD3=M7TL4v#*(eSJLyFN`;NcxcM<;@wL+-@z|koM?EaaMpWr zEY{z~2To(Ir*;UnPrT;g#qCdhoX4^2?8YZg#Y~1OR>_S|k%#ogXZ~R3M5@GG`!_!L z&ZFL`_th=dST=EMxcHXN?+Q6LH_rT>-mk35o!anL>rU^xb-dHt~4sTr4vhx%`*=oc54uWr3NI~%t9 zfNN8_`MrnXNl1JqU`Qx!Mum!?>G}XkxNG|$p&V=<)JZ56k%m6KNp88v(%mST)S3u? z0nuc;)9iIv;{XJ^h52F#_h#Du5!?3vTd^N{`a+2GFqW6%1OGy0!SrBhTg!P_O>=~v zy*_D$G2juF+8n}`;|Z<01J_zSZmlJUt+ja63hHf2HX_G;C>X3cXFTzb8NGDNh&&$c z*~eCUw{|wX20gmlzxogCIA=CiMrej%xh$5+g)6Rft>)-xkYPD10hBWMHo8vuY~kzn zul|GW?AT~Qi28>KIfPhiUP~tCy-Shg5RHG!@SNwXn3$LLvzBwz*nX|Q89s!syWba< ziZF|oKiI2#@Gh5U$?SP2hl4d=+Keky+@$t2Ql~cBM9SN4OwSHnr0#Tfwjp(=v)e)b z&F8t&A&>83bRCH)zmzI-yMlab)L!vDxfXJsO#}oSk5oQopHkRgZ`4)73Tp)0G_MQ# z5`Kp#$PjP%G*Lh=v1WPCi9iOiukPb|jZog^}Y z4G5U{h^9WbDJX*LPgDTqM8$%=2K3HDbeJ8XuhKa%!OIVzZZPlWw-I8Qm1Y+Lnxc*M zfaw}VzTV?dQ{(J^xsV-5F>~Djus*INI^q2-jG(9*bGSTEw{sxkd=cjD+_Zh}J>q|7 z^SQQrfW6V*KA>{JHiJuxBm#%xD#?J%d1JgAh5SKIyUG=d=CGx65!mO>RKXI+%Wkd) zkPxWNK;>~7&>XTvh^WXycJoI(uUKf)yQ_g!(z@duHVu-Tq#OQZ#W3Y}B}?empw9 z!8!G^nwh8r2*C)05G%O{DwKiBco+*nlJneEl5^drg70mwi#C)p);Uke^J!g8I+*6O z72JfS1jXB&O+Nx{gs4^yoUVy=FShz|URr&gm~JyDl~SnIu?*TSIv1GxGLmYgyvbZ z$YY5KUMKS;moq_TD0-Fpv#Eo?lQ@(OgR~nPWmc5!yg7|#KV7gN zbiJfMUCsJ~T&7CWpHY+`9nUHqb(4$0abuAgq0!b!hquZpZbiZ$B*TfTA{TB`aQ17K zgbf+Hf52sL|m~M|Fc&czZ5y> zF?N;2*Mu8zTV~^45F- z?DTu?!;dOCQ7SKFE%@}4Ho7}L*4e`K{cz{Y<<>Z-b(7NHjJ%k`F-*l} z!u)`6JxE`%C;(e!rdR0&y~Gunf6Yr+iP_XHcENolqSxj$l*Fd(q>RL>)WjH zx_9yU1&`9X;a!9s=h<9VPPg8F8X(3_7)B#5f`HENeUI${MunLl3qDnLv-_|1nL77h z?Sr-MzXx&jSO>}79X-CJ{uMh*;1zNblsHvAG{!ThdQ?ue=SmxpM22O)sd0qbETz|a zncJ36U4{-d7oh5y&Q-=I0y0aD&S)RB)>DArh~*?f;2Rs8kEOxg)VSo#Bw`l{&*kM} zLj(4&|MkCBuq|BKSNE8$R%1lLt${f*DgyxXQW+bI=7E$sE3u1M!6+d3w5auxjd*B9 zTTjw9RQ=n({Gb2%fBg6Vv@+00?g5Q#H*BLfkJUXzcSZfTE6dzFYrnBwLK1Or_;Mmb zz4x{OKz`L{yG@K)FI5#VlOU8_0RGBBsXu$N-n_?CQFi9Uk>YgE+$EP|4< zjQZ;#76%&}?<`raj)9S0mwoif;e%*J&3Z0AF@>wHB~22PynJpUB6IH zDJg47;Lg%#D#1BRpYsB6mOkU8rKL}lWu>K$y{pn(mJ(2eg8LuxBwbZ4m!ym^AN-n$ zFR9W3_CT)K=O5mk9=0a4{+Cv-f0eeTVJ}<-`lS$ScyuYX%B0Zknzpu{K#LuP+u=^c zgMj4X`-;Q8J%DL2KOiMOmX{(CS`RZd605t;eCUMFr#a8^dCH)T8zZfAt}h_o8co-& zR7FLQzxTPNs+9>IN%&1;C#}?UqT4Sc=)Cgr)&`>wXgr9BU1TAy?08x6F)zQdv4-EV zn3|)LH-0|bd&MI^pRttvZab@F8929SZ+{P>=6m}G1O>DHz`et14qUkkg^dW zjgi#X%D788mWNr!ugx9pX96qC(uFEl;K`*@SN3jSR#W)tstUsZil>Uuu@Z#w6}fT= zDeWUDEB=x95@!v`knLCCoVjkyYwusbzAmSNZLmA?AzG^yc+stFsNd-{4_Kq$*(RUe zy|@si&l}yaU`Evdyh94vF(O|+&l0xN>EkKPoyeXjFGt?#sJAe@;-mqniiebB1jzr7 zy?0G+9Ld%M|7#UqQg>%4NP+|(dhk}(q(TvqDeHJ}B<0O}EvhZ$1Y9IaARG;VQZUPE z4Krpvzk1rNf6iUPtmkfEdI@8G&hZC8P$DITGNMXZRv`(1zuk|Y=XW|y_FANw2tc)( z>(rnAgZcDM?cuX0F2n@h^mm|$N-X8oJf+h`?_HFqckq6B>JTr@g@Y>5ajN3+=(w%$ z@#%OSsl6j<)Pu=zrQ{vW{XG-8laci}sv#TZ@F9`#K`qPnABD?*8b&q?LUD^qOnkmJyuQ{OX5C zqm$a0Q<&l|V3qfJk1)l5f4fx;3J)*Stce!>QM3RJ0){v#*3Y3AKv&f>z96P`< z=-mh+98wO*ZE4sIMeXIha4_ZQ2xk=z#KN3l(@+@|`2{N>-lFPiWgvte09BwrUhOD|kNkmSO2B)++WfBv9Hl!z4wN;pRt@%h z!dlhzYQ^32Wnr!85~w2_2R*BGI*x_|hn81?Y^- zf;bXMu?xx{o$5qHaQ|{Djk1Q!%#GvZYJo2(3`_lbA8L&^bsc%eH@9DLfA8EGScz>W zL_H@J0I^M(E{m1Iu4O!*2^&vA6O+mi%uw{6Sxm+frb&^;;8zSbi3*nA3=*Dm%+HhZ zD8o&J`n4HP(atxOahRLOULj{O>@$yWVFri`Lz;Snyz+%`S5yOUGdUx87e@m=6WK(# zI~aArWZK$f$I}#R$6-KDIp`d8oMrzx$GkxZ7<|ccQQ&+W_<)mh(DUmrw1F}PF_BBHnP3H;zb?f|!Txt1E=Pc^jI+|9&US@5W`*?9l%E#y(u?LOECwEOG>y21z4Z*@CcNodH3ILfi>09-rgVan)ssXph04Nda z39&f$Xc|ECWB;((e~8xl_qPqKQu#Nkn{i{6e}gUSoNTQ|0}o1cM-NX1`jxQm4LuBe zqdADH1Gu|rueZR@zN*{R0p*caPeR*wG*$7uTB>**qr#FsfrdR^V804ELCYO%Hm5v% zw%m`hfn&42JgR}i>WObLa6GPp{aY|_G-~dqszU#f4IJerR~R@RODc z5w=%fN4kj8kV;1G`Z?6M4rDLK+?gf^LLv+6Ho}tyyGrCuq7$8iLik0X-nV!h@2)xB zB&WUN!Iel1vr_KdYjoTy1p#T7G@yQq)TWeycq!N4>+Cvk->oLvnYhe^;zFmHEQ(mL zkeog#+L#`{rl*xoI=k1+3(x1pR8l01HkRCNf1&$8WW~|En5wH(r(UaXk=8)-fb2!) zf`AyNrM4AaXm#yRYCOWZHpZjIBiMu-3AwjILSA2DT_quRo+ad!60#2xa)GFYBq!D) zR8iVp6BXvB0gpZpOA+9gF`qD=O(+I2l)>(h4aZVme&m)oj!MGKD9}@(tp98QYI&3}=8c>*wP+wBQpCi)q@%-Hg zGRc7Yrbe9%qL?h=^c|pbmBGta(61F0H7pG@u9c-^ves+aNJ!q2Kcxn~YHA=1YmuY6 zw(7CZ2!)<&q=W?7d$*LEVh;+uG9GwMvx60vf*@-qd6KHhNzBTMDK|hYo|&zCR>pH% z)w0(eTS^czF%i>7#Y#zJ)$vpmt2N^ZpNI^kvOJ%QY`0D)i-AWz1eG219^_~T8pY#G zVk9_q|29{DieD))L?)A?nyQnfjFWc;6|`zwstYQBIK0IWH*Z%bexojI zw=0U)bz!rmYTejnG*NU8AV>+9VPUf9 z3U*PG0PO;%A2CUrOiVCt;qU;g+=qwV2Z2BvOFJ5%zE(_V&22scCNzEgW{F8}%(^0li#HzJ@%i7Xhuyc9t} znhqW#a%DabrfQikh+}v!lcbFpUUBv$^&(Y+f2Y#|i7NHa$WSt4#8FY;(8siA{}< zp`b&yO%0dx<3r-qYmRG#Qp7PRqz9EeZ_RW3Vz`Ao>lNGRPdcv*SAh; z+KzTKI$>j;3&;Iyh(Y3^Q6v;{GKmGqvc@$n<-rx?MP3D0eL#L@r(zQ2MYb>wwxqXq z2;D9z^@CI^-zc>H? zPCi|&jxQ@@u@q$qbH>^IKlv#jGrB@>F* zj*i=k<>ok&kHL7Mwf|TMlppvjA&ND48V#!x3k;Q}ChDd6!-nLbzl2HMQ6ftXW`0-q z^&N^N$M1W)ZFL0zQ)6k`e`3>Bs!^UfN+EIwbTIY6ArIs2Z)A2AOCILz_>{?c0nW4~ z2zX#-ZQZa%?C4SFwGSEhdiw{>KJ0k7Tag*KbgljsCdFY#fCAus045PRo5=*UIpqJK z-EU%&SyVN{Fo~(nApJYe!7amf z#!B_s6z2>hcQojS!(ux*>0q^M+FWbVshb-Y(nm>smV^_JyLybo>Iq3iK@Zul*HhazSjeNdcbzXR}9 z{ej=??^ig>61CF82f${b2msm$x}j|e`fvu3kYOg%WjKK=A<`ziYqCs)JSbd_Ts^Nq zVA=cs;Y_D9EB~;(r!xot04%6G+OLTdU4IpxZ=u`!@O%q>QHFG5a`CmYr5g<7_vcQZ z$-^^w_~w~Zwq;y|s+xEg$RkQ9^vL+fR!yJ4tqu^zYR{+4^={SWv@mN>HQoLO3{X8m zYt@mnO_-lr-iNwZTCYcBt#tKu#I!_8Z{w`=6mH5@Q3Ebd1r60zMw`|$o=kD?S^G&;=d7LMuwLxEM6Sz(%CFV2rg?Vn$dC}<%yAd~0EBT~WL z3(BpI?bML{oMm#Z=8P8w2QUkmtSD!147MA?R3Z)loQvI0wci)_?eRK?LR(AgnyMYd z*YDQxjQetZ1i07Q?$wcs`}U0@Eg;vzwZ@iZvA8C8UhHvxTbE1y`o{#QrFpFa)B?t_ z532hEzwXc@4d;P4k7nZIycn|bf2;J$<+Rt<3s$5@ada)-^UtS*^D$gm*PW@e-Re~T zhWF{(BhjL@U?m@syy*5^LUCK0k3=zav-(8d*5p%Cf!q4<1Sj6jD*Mfx`~5DQ`-8f3 z|ELhKxasR4V4u(3^SQhCxhtyztkjRJuVT4Bh~h?OS4}t@JdwcU0H0b&#;;`*f+9VR z0urGT8R}pKk(r8XvK6N$B+C8uCc28+bkbU?k`|8@|Bs+DI-$TGYaDh4cX8N@$)(+m$6BEy zEM2Im9Rw2)#)D$&$s8R$)O0wPv(8r+B`hj(5YLZBC*>4|y76lsM{Z}zYSAygZuCBR5?0f?~g|sx5i_qXm(P32QDD#4}Z578NE0 zIFEh<*KSWO>08MFLt=5zioJN0EEeVz$L(5dAt6 z1uq2#`d;@rF!0F(13SGfLeQ&*+QA;p@QMM18J+>|3@~Pz9Oz^qh=TIDpt=Z|?%)5% zzugLOH`o|Oe&F?31LObjbWC|Zt;7tjM%FWR)P&|4#QmD^Jhe5|g%f09&V0}JJ>TwH zzjoI?H0Q$LIp@NvUE4V%L`hg}3L*tm8O4ym1;1cCgwRZxxWKrsO%a0mb;=J)__gt7 zG}{*$-&js)*Q*^m5sAp6;P~BJ3edVMAoLpd)0Yq291r$*uQ&EQir{>3;sdZuHhS7X z={Os2=(}9L?myYjX$tRTYfDtD?Z_?d9jsv&jZv3mCt70Fw=slnyUDiylEcrnICWhN z@8orAx7*|1ZP9Ewp@Qvep0#5+G4p{l;Kg!Mo!jyJ@)BY_6_56ic{LR&MwF!AQss9t zfQVdl2)vj2aZu&F0B9;X3d3y4K6mV#>BQm8ei7rJq`Yzn!4meu_H;gO@rg)^mZA*X zhCxGWKuLHiAC&}07nl-JxD}VfraTFi{E9F~F|yx9T!w}&VKf8$n(66{7*D0Vs>w$| zL7mu`0bEjd6Sk=&rcyMjz%C*1$eVvTdOv#i=FFRgO9(rTqk4S#9$+p*XKw3KH>qtz zLBS=;O5<6*0_PQHMT=HsQ23F}__c_0FUX2tVTkb8m00`=m{L3!dwopz79|t2cG#g` za0W{Qz@YI`?;{W?U}P-1RKl&1Z1qgL;0CD-S*AGptUv(3*<4+5iYdbON~4N$xe^usP%tw}7$JoX^a{DZimRk^^u10mXqN@8LcO-MA>b()E5 zkrZqk^W=)fpr48o#`ZTBw zrLht63M&?dB?MgnC!^zGNP#IL|rG?!}c1e`#JG}2wkZJSE04aDu+uF$u@AfmqFXmnD>oKVbO zB4R7nD5=~@@^!XI0`|{G?|ueKew!>aBF>WN3PE=JPXXE83H*XSxJrzP&mu5?Fum`F?9+C8 z93}01s(7?kp#IHcAyU@wZ2uS3+As!BhWamHA7m2dL)Pt3Ycem+)U83CMQ^+xh5g&4 z`Aef@g1V?ZaQed^768PsMB3A_%n+TC_@nj{=>Cj(LJ~+PvQp|6h9sam+wA!wb(lbE zti{vF%cDu|Q6?gT)Z11Ub$dsv6caS;sC$^cy!Jh}>Rk&urp+Qhou7|Nj$r_kfqHFq z{IduKP)G-Vlvo-KPV&k6x;%2<-)3@V!28M`W-kyDW%q0DFHN@T$VkRfP!~yrW2s~m zHU@OFSrjir7fa|k7_qSkxFX#S+jA-|%kh;w3`N+~b;}I6tQlVzQT_bgncb$C0fOgw zG0o(BGR44DMYt5j+-|jQ#ny4C=rIz}?~80Mw&CVnhEP=}71f|9#U5xhmMR#_--C=* zHduVpci2OErRr#@N^(jjqbsd|)Td34x?lyUw@NN@5L%sO-hS<n#==6YRS|UiV@g7aOddv`gK7Mc?s9EYz8Aw6$cR% zcqohd$v&nc35U;E~I!6BRqV7}E}wD~C+%jzHh zMUDZ=)1b=t`}-J~UK{I^mY_|)k$JKE?wtdORUyG-=mAe|oPxmsl91E}QG5$A6Kt!2 zkqlKZwfaZ~`08%>Q!-4en%g@%H?}Igyi!ZHduTZmc`88y`}y=89AQY}ndF7wp&qkL zs!2dgml*WcRHOl3t&I&JUNJ%dI2#MBX5XY1#848b z`(xtt`xs=C8rKa+2jvKz)*+>mnFSuoR3t}f^zAqwIO?I!3iZ%KGnfsYGnlPZ5ACC@F`}y(X`#103oxDFe zFPnsU!45nB5p`rDYpG9XVy3zXJXTp*Brh)^J-OoY^&3YZ+Wmu7J?S(M&}A!Pr6AgD ztQuO8M6KRTU6+Tgw?Q5@AHJ<JfB;D{#8fMw=L&E(FN}WYVjW{OQ3g`*be=-N z(;e{sK0ow4()2tS{k22)nNO}aOFAB4oZ+O9_Q1UaILqdVl0N|26n%6Obyumcs;_j2 zKkAJ{7ATQ%0`j`MSM^p`yTWdFQ;<@%h9Zb&JZ3LC-XZ&5ZT=?fc!vY^``|w^Q3SiD z2;3zFWJVPT{0}#%4UdO*bB}s~(BrD~M7|fZcj~3>?oc&jd;>p?WXN7ec{WEEyjSyZ zA{;$K8HitPMn}cqWLD>C)W$>AGTmyYGHivW?$#LUZe9EB-7Pi8wpna-BSyyW`Ay$v z4Zk;_m0tq>T@v+ca)Ii@UQ(t0=)&C|pT4d&I=9xSNP`Jic-`7>DoDjpeJEzqC>Uol zoW}$O+tX0GJmOcZ z@oTNC@vB7|dsY;#t0=6FoPeD1?&@{=D2m-PcUetFal84d!J#DBe!%ouPQGR*ZMfnq zU|!syP+DbU zmb|A*9f0oESvM4&_rbPuxW8A<3<8;X1O*=`K2PtLtx{5IGeH_* zo5!)uvQ4Me(b43dI-bPpI`eFv+?hO_-_RQz()TOcs=g`9^#A@p{*6UR zR75;3sTpj9M)`Z%WIErLPGnNf1#j}EPg7291iC|t{+ILAnQexo$2JlJBH7)JxVZP! z-i3T@*S>f7G1R*#%Pr8J>(py<;w}laZ6|QT;zk-||@$ z1C#QJZJ&&;``b#74?kba zw6nrkFpH8?qC|>QRZL1OZ$ws!s7mIuvB)r{ioPx+Wmp6gq3v406Wn_&ls5_FYIeaE zeaJ=%v17LB%>try*ZJpa*XL<^OV?AQou=fbxhS-$!w%q|)Z0WB@WG6On>-|8e{cJX z{kQGkum2RXN&!;2Yd8&DS23JrWNr(|0v-o*$kvO=`Eu}?EhVaCFVw3U|E!vi&KO*-jgkY^RTyKJUamJHiO2wV6r47EIWur{K zGI0YWh0SoeT<*H(`TJGjY#jmWNxaMt$HBPY>3AMVP7f8B2AFXff7};j^V@OYq6n4s zv1sUfReL4CgnV<1Gd__neuV)Rz{<-cM>QpBlw}4f&=+#fZXo1ZvV5wTBA&=(F_ZIL zoi2{I+dQ}O-yQx|5b_(1=js-0s~TFw$3`+~yO(0UH_bDJ{IV3Xv!^L!B2{k;l?!Bo zdyibt(wJAygu}iQ9zliRfx>8aXtQ{-3pWd=_tJA*!YH~r z@y7{nOuMZ%R-F2)Nv?K^xs3T_S(Ej@{^$So|5$ewb*U)ER>?(qVJ`(D%usq+JvwEdcKm*C&#{y)8^=xZXag$JPUmslRt0xKdN6vRt+%G7A7777n+{n~ zn_G#(d?IX*YYdm()}s=#Lb9odQ_cCkXdm63hf1AVXJU%G>T1O^7W`6Eyl6D`V(*&ZDW zQl68}i^s91$q-l{=9&dEbPSwyT5P){R1R zvQz7N$T+#nl@Nxcq93}J5P58IF zgV$`UmLhr3!C7R~rX*i285+26_g1g}kMK_@#DpA3lu zun#j06Gq&JTG2QfXFOZzk3gHbqW0EwK3= zz5~VPcbX2a#*|yreWbCu`$%KSedHg*B?LkFI)_dFkTrb2yWa#Orp1r5+j#XRuGzvZ zx;Qo=ORcY)C}~k%T~9=vTKA()ts3goa;a0RM4ehIs8g$scC^-^PA!Kzt*9LqcHI`L z)X69S23~)K0^p(HH2vprnw1KG!Lo-5S+hVJsTGu`QWnQYut&k7iriz7@g!Fqc)j>M z%u}9S#cFQ(yl{{_bikS-%kK?^K?CTLHdJ%4C~&P4e{oHF$@LgHcnT=D%gatbI2d~# ziE|Hz*%=xTrb`~hcnapAQZ&wdtyU)Hxo9EHsV!SWH2~C4 zkq#mo29pZ795$=x*X&ne=;mIBnq(2D+Sz4e#cgN955yilu%TPt9G^G~lEC=Y!ms2Jzxn)y@;E zeW$nU-l4svJM<}_s`{7x!~LG;(PjEzO{hPBASj(1))`VJkr!5x1w69rK2ecid zQllOB~9!HBa>URi{DV^t2U9rtFO-u*o83Jkt za~~z_l7ZkbU!T*Mbhy9*~M6Mt(ySRNW0`o(_ms>SbW*KhzQL&yI zYY4R~kY>IDp|-JxQ2T8Pg{1TCHG7JM>g+fD!$;BiHpt$)ktyoy*7+KDtY?c+6*KCIJ3OagC+SU9?`!?MslM>08f+Ow&%ghVf799qji6A!?3S&o zs7OeuaWpzPLJUwHM76H=!mnsA_E&k<5kXTd@H`ipQLUkC9CD^>=VnI=W?pz$7zJ}92 zPZEejRg`|r^XZn_q(Kqua>yy^nFFRJLhCwDBk{QqNe+J@!ir+(5eQb~o*ICiFnyJL zVY^OueAo%O=aF*ip*ksWYbU=4x>na$MAcl}Mzqss3o2!qNM#-sQVFHZ`Kx3WhH`F{ zEH|XjXrby~lf4A=BZ`qJ=^jDPA+7<(NZksWt#%{eT1;ldHMGdnh!Q=A(M040i*h5} zHQ7alo?PfqTCE&|dt+*1*HQZAb;(GV^e{D(m(Jr@D2_EA&&lNnS&Y!34%V}~ z-%~5v)hU8J~#)VSI8?G<8A?O>T&NMYuZ0HefMdawV4GflzV+P=q`e zp~pTJQ<2?7xoA2Sf~=HGFeuqn*JM+EO^t|IS}d@}CL>vk@;1=qRoXcrNruNcE|5vI z(r{UyYe9NSxl)HrB-c?U6SeWaPcs?L!JDUvvefq_ zHwo1JWEo#5k=D^x6cUvi9y_^YvA8U7KCBjHh2q4K10J(l_N0D8OF9|5WuFKP2~OI< z0i3jh!$&%4?)E7kG}?HNchGEwkAKkW&cZtC=jU_sROjSmWN*qEEH06R2pyn}6Trxd z@JvI5AhOvWr1tCVKYaI}%gH1bc?lFo?Xov6d?CTHId{Yh7kO3B66MIr6-NSUa@fzi z2B7U+O%Vxr!92@EQp5|Rv37fKemo-g{v>4Q@6Iqz$e4+$IB_M9xaa-2A>BSA{}u!0 zOAM7(A}V1YB0AI!ZB$n!6i*%Bp(rLbhfy7dpiriP!|s05@3Y2X_keVW;&YMlk60c% zwqw|ZL>7!AB&(BQ;I-N-!IbfwU7VG2HeA4TT#QNy3-XICJN7b;-&Sh}4}qF1!24j5 zJ|JWhf=^CUuME{1C3(SW$hx@YfJ|ZUgW{^BClNJ&c1Q?3ng57|8*`QSkIy`r!r)UJ<=|E zFoU6DNoM>n>e7a=$yqd+7VOhZhSB9hI|actV39fP zEamr$vF`2lcG-&vtnLetxB-JMAdVdF_$*iZLCK1-OePvm&L21*?0Q=t#I?wZsfb0M zlhzKCX{nlcE^@f%9FNmzmhylybY1f(rW73NMqkKzW~G@Y2%SD0>~%Vw(DO)3`|#8U z2VF3CtDpXo-#r62DKX+oigEKjRyZ37rG@i{hI-%@2 zUs#~IXp+cGgw{(G$UH0X6S+~_asQOT7xEmz@lYP4ci3-s2dn`<_bd5rjm}Qz zhcVvrEyi}-n^muq+Ejq0op482>o{DN@gMG28gpx&(1&>m*A#o@B>#+ZeTSC}Jm%9A z1BC42{18?q+qOGl2-XTz`;MW8eTnZ+bi)F9DHOoOaco@DhJ;`+n)Vp$8tnMgAnAGm2m7QEwMtATO z-#@%_Bv@@UgR9Eu&XHnyOvz>G+lUwY*7>!Vtn?%2yg>(a}>}g|e5K1#k;xfWdT%Z6Ldl zd&?_Iy;hng7xAA}QKeWY!U)5+4EQ&x4#=bqTJo~#)HK7EwaK#_4kr;l>*G5DNj z0;3~9L!nuc$DO*b7a0Xk-2 z&{ZtEPD9OXBu^@DdK#2Fz4^i*zuA3IN-HC2i!c76{Sx_R;>J3 zBj2(7DoWWkVBfhPu13bItL;jZ3SWsu-5jA8yiF59+kkPFimav8{71*{$YCCXW3}}+ zx0~g=+?hAmHfq{>gG{YC=y@HXLC!aH2u)oMaBSBxcM!6Cb!VJ}bXCw9K zXmsL0i(F>H4NLY1rCQ!faKgd-9y}y1=;a)8f2bP-a%VWz!L5C4-qc+TuEJVh0eXN@ zQoV#%KK4K|R^iG!}65p@OU1&@X8d9b+U0)}tv25sv|MjCAw59}p{?I(Bwltdng zQBzgdrIUD1FAb-3Tg0~C>Fi@(=Dq!Lw#3^h5VUFY)5;8{3v69Es2)rXnwS@tGMiPT zBj}Tk2D)1&YHX1e1A+h?dWMg*T@p^}9MaSnC$AlLE^U?{=#zmcZt6NcS;VPMf?F~O zIAN5YtB9|M7VAAYhhgtIhv6+MQi8L&J=sQC8Qxh>=T{Ex>04cReX`2ug?^NUu2E7S zooIy#SeeNh9K+S&IAztAMJxQ2Ct-}pHn6A!o<5#Ov9-v!P;!o1d{!l%8hB@8eRyvP zYRBD|PoiY*Umo@jyM50iZS#ZCy^{F9l+pdE^Z|`!eSp0iYh>!0yYW`h zz9Qd1Ri+NL8-qAflGDj+&who^PQn`gyRIxk6fBiTKgPT2&=Q)rEVSKKlqU8LF8=n` z>e@mUSH_S>FLJq5w=T=&$~d;{X|GdTO%ct6oENv(a*iJjnPG9u0$r^*R~6<~h;qw< z+?rzC8bVyX2&V-&;JU*ougQvis-Cmsc&=FHItEMaC3rc+qPOx`4s)|AhQ9mYJFokE z=dECFd~5&J7g;K@8BY|AP&*(4EucXcA?>*O|Ef!EIv;BW7nOZc92Jq!rc}iT{kQ-A z|NX!J2iQ=HMJg4M{&60KB90P~d$aHsM}lkICtv;+>-BmAv47xsbe%o;7TZ5;_Eu8%rVG)p@8s9-ZBa*u+{Wv0x*2+yqr@j3>DnE(eF*?xpZN zx*{Gf-(UubzTfma)tjLLk>;M)$AUD?cO;C8SSMag&$yCo>lYqqW+!1riQ_*}B@|789m2SHWL2`5(6 z6*Ts4q=9PQ8u(GY4=4~3V6~HX*T#g_->^g%6htM`tZxLKrtyLq$Xqs=L*OMgw6ahb zmg1h>qsQLzynqi^-v(~kqmq^ll&$2WIZSzQ1upB5$xuhq=8DY$HxqE?gi6#+P_L_9 z4N%>)0M#q*88>>Tywbx=1dAZH=+!n-D3deo|G7F7qN0Z;_NkH)F*l)=)GGvSasoM* zHZn(FbQtZ!I7`HhiQu)c5$Y)gC^cDr6BWT! zGm4j&QJ`v2H!Xw>8N8)56W5WP=S(I7y_QC&Y{%~&cF?(4W{OH18nD&%*aZZTd7nLd z953A5ke6JIS7>`8kE2PV0eg%Olm?kLE%rJ-#woKv&Qj>#SX`>++l*fpLhWjyNyyRVUsR_w46eSvw4W(g`S||hEK%&XYFdP!B zLouEcjEnZ)xT8iW;nZhuKq3z{pqZK|muO@Oh2Z$~b!~^&sfeZ@eAVz*4cEHELNSxH zcuWm*9+O48CQJ_ts}j1vwP9d7Uf1)tTD8Mn$_usLaO?6yu`Qi<)9yGRdPeE)EpFbv2LWgy~-#lQAo`2Xpd;szYGub7VxbpL92`b2Od{nZs<_ySD-vAadi|Cu1I7a_27kr2IPGE zode;*PDc5z)-Ta4S|48ZLIccKvyf_uf}BK6`Yf8Uzy6iIh>{?lhl2fCdsTRfM5qt` zyv@8__P4)nm&?IDhlM@j|G>izp$XLIjxB=rlk3&vq&{fW)eCs(j9o^ts7sX!FKnC& z;Li8<0e8ONyC?4aibw=@3wl3Z>LR=i!GA;iehr>?l%{b6B9HMynR0cTUWvt!K%olE z*(or;RUETCM{TrLB156NvP%FyG87}36*W5&!hS_YTk@aSEJ~u;d^YUpdqtH4 z%af72=*3YO;>qZG-6rJJQq*w9uSAU@BM1L3tKP~zUCqk02Iz_3cFMGO9QhB+TYl6g z{&I!HVa2>y9gBHe9*s=IhNCeRohhP`ivs+K4FqF8WQA;!7i!a~QthrZi)0oR3#XQQ z&x+B~+h8J%9e*ja8=i$CG%Eel^_k5vx)8rHC&iHj*o%K@z11JIUhbOvrch-?crwD1 zsE)?L-eGf}H4gU4%}mK7NmMMbJ3CmeRRNDPFBk-56v33DBT6q=4f+w@FyH<4uT~5+ z*PD8`%{=zEzkRpWx}zQ~VqsmS>yG{HZ>?2V;CEY%2Uo1`|sQX9W4GBwy2P(PxDtR_t5iEZ{hB@oo;IlB+G>vW&(I`w+ae#`GVdd{*o3bEx- z>j=tt2L(@E+jl!9!iw6(ty5w@FYXs!Tz{3(4*fIglqH~JrbP|eLKGY8g_lfrRZHOq z!n4&vXOw7qbu>Cz3gE9(6km{lbYb}s)jWXBp$69`ajdIj$QAE6<=IRo(QoQWlz5T<$^GF@|;5hxx zpt;8y>c_zY)E>V(0;j!99}TOy!EVE9#!DVFCPx%`SJC!FIXfAlihUx$<08TVE7n49 zL#)9wTAwB`+!jI>Xf+))P>8EKabvwW8D008%-G53de8E@QEDJb2R+EHJm?Jw=E-Iy zyoW}jtXn0=pY@;>yj2>F#zQq4jm zT!d0Jq5pL*vV|jqg3T)vAz-(9wVhuoQb@YtR4LpK{8Iey>a&shs@#KfFfU1UngEi; ztPFa~Wjqi~Cr{c~6gNU7tV7-;zSnURZF(JQPzfui;M=sc|C0G^T&KSHCpP1s;a6WB z4q3$Mu%d%mCy}WftK}ecx;CB4woFA*=&YITTI^GYQ?K{H>OR;bq5JHSu#%bcTiv~i zcdcfpaQiJ3b=8)$ypS26RB}=|92gkbt!ozXVMw;*&C&7uH?I~Wo>x(r3=5{ddNWIl zg$Z?0XUZVUtd|@sQ0!I_5l|wSR+kLwT`Fl8@c8t#M*2K)GZk3J;!N-my=vr5aTGq6VslGrI>eb#0r&sZEBjyoOCBhK{UX6X*Ze|9^_kZxv_cA>mW=0&wm^GxJbyLI)V zd=$HN<956C;}~s0i3xcU8V4PnEJP7uh%*+$EjJ5jekyVqU&DorkXsnDeF^5u52&WM zU^m%3$xXk=w^x10NTF}}{jC=Gcc&S|2(x*jRw6VnNtFi zA@JNu?BXd+iCSW=8A<2Ms=WvH&YeNP=7ZEB&G~vNP}qGt~ZRU|)Mq%`x5m1Lj(o>8e!6AlV&y0|P)1SRmAf zjS@|SDlCGnMeXH-&Oyi7waoAi7j)Jy94?~vjz4{MbPOfJC?}O%cofXU(zUKHb9Wpz z=#~t~)m&?in!DCmNY*3=hQcJZG#yiiVmaZd&ZkDzzDcgxKl3aCow4d7f)qjiTS#6Z zl540$&IjF6j zcow3^d&ZSpJvx4)juMN-HLT2?ObrG#{7kb*t{f~+ z0g=mrLu5$Ki$@M)ICr23bzF1Z$pHs{3SGdUii^Wer*olJ`9ht+izfSv;wivM)!Skh;Y5f~gj+qFuZz*g*S~%^djCczilaZz zMn}hQF5EZ{cC8O2tW6?sU=(9VH?L15@r=}y6G&H_NS$aU5sEPLY@rPZ#Z=5HjC3v` z29S(?7zjgaHP1z6TIhT1Bw@eepT9QQ%a@8+8!G+srL_$bU3P*y6fc05HHhn|6NxCa zgGMqsWuJEZuHV03TAHrk-_>Ey`Q5=GYH_FX#@S4IoLd90(oaQHjkV9ELW(%B7lv+7 zKFZzF(o{lo5Q7h5D<$ySxx6fH)S9C^xm&$VFs%)_`oPZ6a%L>36+F<98gk_ztMZD3 z>CEipV|nHmA-Q#B9YG_iOPj_Lk9Jc&4}D$b(Y(f_A(ji z0&eAJ^5+_AKvV69>MWFVsj?*p(@)iL*=@1Hux-}B zrSg>dyS-zoqu%T;0lzFE_L`t>)n9cJ)XY~YL~8v>b6nzUqMwk!=s;695XxZkRwj@= zz8@32-H(afz&hv#)@%BMZ{82A;+EC;UEQ)8CAX}lzWhY)@SFC?R9uQTELZ;=Z&(Iv z9xg@vts8Gx8yC1b-f+`+!Cr3w!KeLwyvCheNrn;l1ejjP8{SDm%WgrcWP1mT?wVK| zAZpIx7Rh`z{IsoP2$^g*+4f&@_(`5iHSTo(q>0Rs>0(<;R|o+Qh-(om4wcMXCm-H^ z^xUlWF>EQIZLyq8)MkJ(D5buB^Xlgxn{0IS;pDi<-X5JFy=$^Jr>7rJo9vgP(+_w) zol2qXkZr3++smU=kDE{sE5RDEdT6UOHmJmLF}Bt*(Y zku7*ph*?_XC0=nBB_XA6w3mdbbj|C%l9zz`aIE^!RI3iDJ}?~vjKx3@8V5_@fw;bR zp!Zt(wH}w5k{%sE{L(n9eSje!(U74RS3;yqod8odFPNg#1zgrp5L8H0o~9zPnN4bE zvD)q|N?y~1+E$lZw4SB>s+^s;JY>B#x|!;7RXOUMntDg~!Mgs}D`UzTTG?}yi3WRn z&2FDH1_yrA--FOdvn`IY3Fs||yW3%`#kO0mI`jqN*FE|fC>HK}txeE9-nMnba^e92 z{2nNrxQqvm6Q@BU%6B;%M+wgslu_7`cJEn_Iv!dWBEQ^U9j(HzTP=}X z54Tz^VkxxZRUJL}?rkP#Lw%4%9G;3xBiX?NB&Rb`@Q@eWLs6_+9q4f%R41B|8>+Xk zcw6iN(~BxxMD={nQ^>)M5sBUjAnkeaa}=d}FP)w0f{sxM(Shlb*C!L}D&syl`@ zvwpf!%=mJ}CnH_0&P0@pqUkcuz?g@Y)$Dlizb59Q!8rSl-`r=7z0Lvo2RKpom{G@(J22)t}b?50mfmB}kTuFN4gy{j5c*8Laf$D{VouSdHpfB=kwWY6!z zz`FkOz?`wz{DPH!P%luRjntJ22o7MbHTF5Q@gP|w1vIcBmve;aJ{iHfvUjg=3aqtv zqqT3`SynT*ckZ9fXY}`gR)7T@Krr;)Ugtgw*zfoLK3DLb=i>KT#r=bhk`MO}NIvBA zLgoRFG5gF`#UH7xe{TLHS&r)LOk55@?Oi{Hc`mXRAON;a%|+`P z>dD(!g-uSY6#;o6m0(0L_S#vh$#Nms1$kXwC~mN1_Cv+;gF1xya@Xz=N5S4$Ag*hOwUP3+Nwq9!kb)O#E3x}*3hV0g0@yOaO=in0>ILlaJtDL2%Xg217 zO$C4sWfhRwyPcI_A3XuGcW;j$hN0(?LHeO~=!4E-^AH`15u;|=s}<&F6YERa{smk> zRV{}J_L=X2_;Yb8V?q8x>ep8hIOSb{SDN4<_+1e)jPt4UM3=z4_pIxIdgNl|e!5Up zv&Ir?6bSX<473elSNYgfuiO;%=`4^b_;!L8WL0Z~3x8Lup6_)V$UgWG4vqM=c=@Ix zD57f-qj_VVi}fc9+|yP}J+UiY48IP;{2r5e=M?s_k=}A6tEaI#ca96{?s^jm8}jAL z42D0&qPrd>k}qm;&@p^{}P2c=UT2 zplPm1&dJqW8O7Uf4UgnnjSO2zmteiWy6^QLdHHHL4)fxq7igQd6k#+K=Ww-x@VvErN?gfmBXUJF)GF|XLq?`ID!TU*F1 zirUnbX3^t!Z#TAysxoA4`NHN_$1K;r=2<(I6Ek)Sporztg3ofep~}+$LW0f{zvPDl04;a_5e(vu)Z7so=1t zy(M^GXGGf7f@T%Rrcogg14br^2oy8vHf>ur?i$u8>1d#JrhM(l1U53N5?ntdwP~uU zw9+t3u>_!g`$9A}T!zL27NN1(5;PuX0UFEG-+1)NXQq~#22!5qH!=$!)_|_Xape+8 z5hWrndvUo5$&`2_FEP6=DrBe&z=?5#BL$0fs^ge$teevrHBr|g#%3J2Jh7Zo^ykW) zUV;q_>w-kB>i2+vX|=nX(V&9?O;3iI+1*Mfh)k6xzIw18XX-Tqaiy|{3`%!w#h^-y z-O}_}FSkK>f3gKPEtZ$O+>xhPbkkzpvOnQEnHK9-3b!An!9}33kV(7oB*ozX$u3dJ z+*V!a47s*mNRe9|YcVyb@6P?Nh6%XT*2^0AM=)0_ZhPVkh0^P;ZUC77pwv!H?JFwR zNqKLnK8vnZomZl^*Y)-4)Vt+sLAkS>AJlQOL#;XlzP&qhR|Ye&DIsK08flt|9K%w^ z)^D?Pqmny`>B$}Ggdes`8r410V3j1wkF`G0`8%d1U{VubW34~{~6mUG#& zock>2KFhh!a_+O7%bs|_pXJ56YJkK8=hMmg?d#*s-af70d!Pk5m<%oRs zuSf6SwQ!fHyW%p61^9Kp7D3E2u_}fR!mPFTb-u;6TG5P8#Bi&DKR596q->bvC9g`O zlYAy7QC?&V%=U-B4{6=JNUkWFiB#rMA$2+Zm&vhX%8~O4=8G|eLa|5%p8%(#e%_@C zsP9EOk7Jg~I0~Q}>dz7UHDo83A7nAY%bCzhy1iz1pEdAD9gdTdq#*AK;=e{flR`DSA*JB10ofF0#kZ+^5cI#dH0Ux(Inw9i!RO5hLQlz^Xa@`p}c|km_lT; zC;{5{W5xq95?Lg}Gi+|i_H3_|&`*73iJ5Qem}#Yqv4#|Go&cH%bzSxHH5W6PoQa}s z7Z))6)cT0bK&ItP5uj@x0T}WYhGU-!9xhA|A@lIgjzty1{6yCx5qaLu1P`MGoSJ1; zc!qLNG+ZGuHZ5~#E4bs9fr=hnB?h@+$bABUs}LdnPV$hwA^;Y|224e!$XU*&g2%-a zPy^9x)e`L?UN6Qg3=lJ6bGn6`nAG)VNRcgYMb~kH6H$OFCnbFjrm6}t)+TA`Q)80{ z12?|hhxJdu+Ef1+eZ6%~OToxZ3?{mXa=}J2WGAEL>HevLCAZ$3BhcJ!BdJa)wu1uL zdULf#8#ootoZW~qiGc2mEe-n2!O1(T8?tj)crjhtAG-XqC#I^giS@4Ogb7%E^^;QV zMP=n3^g#3FsER9CJIi)dm4dWBE0@RVNNqp#cWyTttn#=XN6@)c>!Aey!qwqxDPD#b zGGmI;20UX?@)s1!Yf%MnZwaBloJI_4iu=`g!Gd{S$QjeQYt#!z?b+Hdb}shP6PVymEb)(-aS_Zk2Eb7FWNd=w=| z_CcermUShT#*#OGW2wot1NloAcd3)?78~Sh{&%Ya&8BbdQi>cW_sEtG1A;e7dUOAL zNU&a%6IG_F;5iTjw2aIaEj$h_fYMRpPaJbTq;J`3Y?j4EpVD$<{5%(w;51&a>n_XW zybu{dXxrebb+S{MaL)F1w^hj2b+5gXh?~}RuZ7jM6v-dQqNVi#Edxq%I*-K}d<4g9 z`KdWy8zrJa)ZAXv_gRB}R&aBzg1HXYNN3`w1#A+*arO??wIU?b&=Hpr_OcC+?I=CsGIo$K*6o@l@8f@R$TELf%f|S z0j^$uP_ufh4A25Og&?vXlG0=a3sF%6vxQ2tTpJv;8IZthI-QbB&c~FdLkGPhj`k;w z;OXFKTR>yaO)YLodY+2F03CZ41w?;&?R&b~U2TLiK%ce9pQfj0nAQxoJ!DTd=q8d- z+4am)|3~!xe33r(7~HCN&GZ^n6@BZPDL$2uZyD17E9wmcD5S%Kks?QL$xjyP_L2Mk zHj^`RhH||KrX$38>9wdnUqcP1KCx1dUKBUOnwueFECLQmbKE30mt}}~9)==p>bhmd z7IMyJe4$g_fho*v2T%@-c%B#2OwK1$L=Fbn`!HMu>t@KGLsTi}3t(w%t7BCI7*{!D z&dBLd@9l%mm3}TA>K&sLdUs|lD#m!I9_9j_;-VxfBJSAnFm*L;_VagV?`0^yz+QK1 z-t}y!)uay94LxzNjE7r|(x02&*z4X-u$q%~Z!GA`hS!#h(0u#dwS1o-cXf*(rw_SC zeqNU5S{efy+N56zYKB-dpeeA6LY_~@l4l`-zxxMGzt0-<^M@anOE`ND0e*Bp(Vvnz z$i>dNBuZD9uC&S)!Qob;@?c|*O$$b>TG6SD<0zR>?jD)3kI8Ehivr35Au4|Y`fc|C zIi*w?^p1IaHqR!4^v^Zl-J*M|aS3$lvpyed6=z#5s{zU6SnayXgNkKaeZe0;{J<(xQaf*2L?ssa6Qn1Zrwq!hp-a*5V4{`Eutfw* zWX^Nl3wxegIX2S5tXJ(FMCq?Q-rdp<^pckTB+_RS32Zg)l~mx$O=0U&Ww2!|dqa)4+8NaBUX^mQ*Ao;aeEPA)-$AKDQW3rz}45qj*Swk9;43UzWnz)LpO zHJM_ruo0oUjx{zJ@mg5Fb_=Mqb5fvn5Hc|40MSf-sZM`o!d~mE7&w%ltb)G0iZIi( zc;t*%(Z4J`auActIBRYWwC~eQhI3#@n=?^NW%#{gIMZvNWs3K>bdp-&aW8@?fW<;` zQkbHq(LumlBtA|#lZ0bMc8o{M_0eLrxjt~7Zpi^sI%l*5Yn-)m7K83SU>M?$-}ywj zd!l@xsOyh+plpSYf1uXf^XmzlU;BB$j7$fxhYk+v_YhknJ(2nFDYEN4%8C22BJ0|S zA3Zj$?K4PNiZydG_~GOo9>U}u9^Q5UpN_0OdeR?E?NLhlL#Z>C5&>oBHM4T*4< zy~yuYzFPn@)sHr*5UClOGX0lpraB5kM##C^N7;ch#Z064p4ps5C7}2sjXmQqSApC` zCnFA`GD~@sF%U116P@uC{ZSO(otwvAA!jjp*C%e&+5=r59YM?IlQ(Q|VfM3tWHSWfiki9D9E zI?Fbj+ZWC0^N`FqI-!biT`^p4zs@-)2?v1g=?%rZgkAemx zFQcI01bijAg&3>LEwod~y3_4+n!Qe^)$4S(^%s>SIoedMUX_y9_?=nqX5_16@>!j0 z=ZmDkfgYAp;9z5SX2l_X(%d2C@HFFs0K8#7gls@f7G^S4!gk1m!igiJjSE%*_|m49 zGfS*55#TkEnSd|`;Ir)aOI0nOsO7G>X9t>RrRKrlrEP53tV!9mIcF!<$Z+nCJ zuFG+a$6$T=*+h7%&XjC^pTKJwE+6oxhc;!#rh+RD<{<|Xx*a8xRxuSVtQnL2sb^Hb zF-y|lkGQ-r)b*E_I2_*r8Un2ZLg73t(TyToFcXgq{3xb6(IfWs6{)>X zhx!fCww>w}$ppVU6%5 zm0i@zlyd(Xn~mG$)#``69wNm$6H@Bp@_gLCp{1#+jH)_QceSL6%ON~BAI zh1q##Rkk~x-|Tq3rtfu|1FvIl+%c9Ovh9vHX!>5K+4Tn7nw`+^woPlmbuk3Z zDIgZe;RZ!B7t#UAlzwqjsG>K7EannNBT@-J}IQ8l4=g($vxF)AcrM zUYe!ygwuJE+7;^f6xO_^r`{UbV_5BM(!AqBo(h;%#Z;RW$!ebcMULS_ng_7=t{rx5 zDifK#E3a)HWIPoVw_pBJJX6kiX(ogi$7f!r3)OD^Y3EalNZP zIVZX8w`Y&6J>PBX39A~ zWzWRwL#xWxN6XTl(|Ig@x2?O*;%_{9MqcY7FL)>?*1LUYhf%6?KvBx(L6H;g01|Bo z)zfsbkh%s03S*qK=#&T?)olH}!{%VX8vVU}_49`xN(`gJ%xbMhl-Nsxv=8cT`Iw62 zT*P7q30E~gRX24@3$-Sjlv$Dcawxa;U=@#|dr;v~9>}{u zA3mpX6hs9Z%Xt#=EE34cK;Y>l<1^S)%S-gh>vRT9=5>7aPZxew|ENFVuR)XfP1aRE z)vp~wYZKJoE7Kf}qUJ@}=5Ru^PVH+*&}k+Pa&bZOLRqsnk7bP1#Ob}?A>Kco=hG8& zp){5r8P*HZ)RoB2szXvel(+PVAOe0mKOe0kJZ_6`Nx$|-3~u?;u-*RSROZE>hJREv z;ZJ`|c`^NyGfc{@XToEK{hoi&zV_QC`NsmUIcyL;C^;aPef>zP#hC{#Ob?peGc#c+ z)osgSZf7m0H_CsTOF)O(fql;bfU#NfPy(!Je}hFaD*-S)jJd-{w6Z(EdXN!!V(P)* zyvPeNV`BiqEW3JqS-!SPh+S(n6s|z698OF1b8ot1yZPlOaNZhoyTJ&hR)zH6f>hr5@KU97b!_Rs; zYQ|&{r)3dmy+FqmqfgZu()n%Iiako$8gvKE-X3cVx;^#thaZ@kt1m;|pE*hR@0pW) zCg=Yc}GRt=mN_*EP5zbcfTab}mSUgK;M_U7JoYjTVkQ zf4}MbUDnw1`_02%d1o=}edBs;Q=22ghcM>xR*SthnDYhCCTePp-?SnQ2}Pm7{~;uWLw)w zS~g%`KWp!(xohdc?P~X0-IvWL8Ayyaaz--A^^2o0M6{8v*KNXF1w3IhekE8cvKdd* zuF14@lwC(04PInS(bcSS_RP>(qV1Gv?_{Xta}E=i@8_ zc3{Z-j`LKHq2z_+Y@+jQoS(0aLRHis$oQ^`AT2dKX~ zOyS80EkNdZmC;)2&l_u=#0>QsrG$FMQl8~nzo5Qy%#XU7R29_a?%yeLgEb3djK9m{ zcG2(eHKSBPrH3oH(*{umcaUxm9yEs=^@OpKxRUWC2WmtNSYB=c8u%xc3jrahHD{+z zg;RTkOjBFbTl&*n5CmQsB@^e{>#S36UPf(Y1KbKc8YHdF@M8 zurzMYHt}sUX7<_jj4%Su{QOGz1=VFLWsWo!}^8EEOOGa2-{eEwmt#_Xq-~;E? z{aJkzK<8AsDaU}!aepiUo*dHrRt4&PL~<9tB2&DFsyijpzW0O$!K1XFI9q$ z4mpHvI#qiNaZKLvg~-@Gg!Hm;l@H};-b> zaPsc2fF_u?`}^Jg;ST%}$eEh8*5F|OaIdr9_rE=n;i)?89F&$mDAe!Lc3(St<67O_ zWYdB}IS*!HyhwST>&}(ao|Je&*yffSUF%rdkMDahZ4LK7nzoL|IGj#xk9RzmhxsT6 zlE=$dug zeU@#%DcQEdkgDT;{_YH2_-pyld+m-me-@(e*)=$?U*N#TxdSS?be&qgE68)CpI2KV zp?tzz?Gse#BKi(E)#!={+tf8m1|lm?MK0rOk-5)9Ipa~{+)FTKNJl(m0t3JmvtVB` zmh;5TE3f`*u}dkv72m7ZW1cO)-f^^n9}@w3bPmZh-bY~=iyNK^2#_dg7}it*&TJKq z+uC|%YZuL_(CK&k4}WGS5NrvMTOw!33$i#$uGm-&8wBioD7--X$VOsba}QmAe)RLM*HeCuAmOM?Y(sC?ylzX~*q=H@9p!6BO}( zusmdtg{wd8)_v%=D+uhu6UexPI*URwek~)& zoqHKa0r{ILS7r_lF4Rs4iWr(}5gT~HWJVbfj6;rw$HQztey8aVSY!X7uYRhHA+j9ADvBrGaexLi zI6z*=jNC7*pGDpTzWGAE{MLd zNy6ZDn+G$|-f`(#u2dFIm@SV9*1fxQd*C#RyO9raH|17HgKA*fpM=o8PWFlAy*r_ zP(8nZF2S#_uum5dS1Nd@R`r4sQaPVGPjm_Nu{h}WU^;!m)WdPOk*#4}&g^+EveqT~ zc*7)ZmFVoPxsnL~h0lv=0{{NjR;{*ooJw0*DeOZ&nPg(Z3vn)o>=WDO;VeqF8L)HZ zca*yHU6ezjOvi}_!QgX*uj84<-HOy$oZLa+1Vp%s1sF<*{N;)>PAJobxE9%hs-#5V z>eo|pZ~uTGE~+;;#RJ^2UD~lJq|%&cQL)e&Dr!j5mi2towi+G;aD?fUl4}y!V=%vD z#`QI*K5>SBG&gNwXF!8y__(wd^v?6+1qZ~nfm~>O| zX^WZ8ByF;bU?{DKXF$$0hO!zFzq}Md;Rdb0&RISWri|z8qKs!jr*Tw@fzL0tEQe;W zP`4V(&ot>>YY03|z4AF!Btr2O1o;UTmpEoLKi*bLe*nS9PEYEeFC};SX+Exy{xyO# zTGRQsy~U>Uv6C2>M52o`I9M0Br?13ldpNf$BI0ZIZ;Q&S(<%2>W~VW4wwhGC^44Xe`BDxX(e#*`j~<*FL@-h zLb`piT`}<8OAh(@(%P#_Xio5O2CYY3kJ{}S?I73-Q%!xlx*pT4c~IAC%Jck2X5m9y z)%BQG%>$>^T*=yrh^r3xnjE3iHn0{m9wi`a=DKHqKf)m^cT9PdG$9DF+jy0oQ6u&J zWNgOOj?9ItinY&{ebRrftc&W09tea7sNcyD|H2@(Y0m@@>h#>1rPy87xuwOcUq0&E zsIqtu>6%`zm0ep};rcbkorU^Q$}A7LjU5MmxNLyXlumJOTPrHc^1=b)p>}HGI7WBH za3DGP|JnPu?zWLFPY{3Sr--DwDlLNm2~yOBRjxit$Y zfkuFod8%ryd78)RnVw$r&@*f5sn_(wyib3g{sMhI!mQXkZU8}vy4ZOcXRc*Q0Aj~| z$BupbX{&^eTd^&>R|Ln!tHXeFQuhCMw*T=!Lk_hWqZyacOG5W*VnDq&JG!bQVJmCn;(yJ z**uUf$Z`;bLEkF9lle3dGt76H@GfdNyUXz3g4FoJ7X9lv6M|0J3#9PZkPrEVLNfa6 zlu!AKUGmM3_8KxGfA=B``u(8i_qMLj7B}_%)!^k{4Nf&BRj+gS(iCgA(ywfX6y9ZT zx2{1AlK&9jpC%=tHfURZD}oiBr)1ppw3TEr`$P^78Kco82>Sg&KR#k4?Dh8d_FBI0 zH&bhQp0|M_!VUrSIR4lMB1P#n$jeU{tPbLW_W6={1sYW$Th#NP^N&=W5ebHw+8@aqx( z#gF(yKipGC{79#CHHtWSl3!WyU?~l2Byl|I9gLvekz`MCBpHtal#zQ@M+7$(kXNg# zl4h|i6d};>`k6i=|LycGr{Wm}`V828!^5ev{->s!;g z-dz!&=d?^P%MU3QDYLQj55=6j^$JW!YC!eXj{Hhc z+*Oki!(BP}eHO@mMf(>D*YiA|%C7n7D=ANGE0@A0>CL4`LYndY?m ztfPDuo#1A-5~VUZ0w>e{VW^x;*K{VobeLToE=1a@mj-3#R9!BW>z{+h2Fsx4AzL z2m75P;vEk5RC=o>|H9AeU1+`X*5V1YCl&zp%BFPC4IoWLW&UI>>rQ|~aI-{Q=UN&S zT!_jtt<|LfadcIt4wtwy!%o%R^=1U*LT^zg(EuX#s@Qqc6xEW3Roui<)bt0ptRjQ> zY4=KX>Fb?c-D`p>@)pvP%SFJ%9^gXQY`SUk#-@SbPd_=C@PF;7%wRVL+{|<-e z`Ezw}Jb!K@C?eoAWGZ?D|lq)hI8RVHsxCLj2NUbDSe+acuY6|Ud1e7}u?wiop6%!XfQ_KVMK z|EPIpwXQ#7H_q%X@`ytqnAU~Q7Gt8Ow~-7M)yo8!p5yy!ECC}$W26>EXctAZ?da>G z_^gY<+K#|oLmeOhVr8y5*8DT7A7ld71?0P~h)<YPsj2D8|IT_R=s>}`gu*t8O=;MR;vog=xFye z;)I?JC-RXzW5G0l15`H7$C!=+lo3KKSg~6VmBfpH@2i+ys|lAc%pXcEcf-cjd?P=P zYW#iwty-WW2pR4a^o?P!bX^dnO>InW@ zwE#$HuVAxyd=*bk^W=A$tP_-zWbsIgX3B2j2FWVU1x~VQ%5PZgCdqe$z#1EdW7$#C zo4N^fAzhmO3$XFE20OUK(ruU0czpS$16-T@$U76!uhM?VG1gOEF$;r*q_wj{`bQfS>Ahg@ zkvh{}u=iDGx>0A^J2E;`!RAX=oXFcl!N`Orslr|od@dL4G%1rgXzA!w=t5S>4!YAU zw(fy-?dqMXL0kkIRD(Zdytv^!?#eFtF^@!DOBe>>9tO(ca%xh+e_c*bvz!_fv6zXG zk~#f}7i-$sfc*G6nX1fv(?t@qJNfi-5QN>iF4SEPx;x83&o8;Z9E7_V*A7Zqr6MjO zGNXcw8Oz*=beje`Xl+pRj(L+2y%&6f1&y?WR_hGT115C*lN54HKb7Kl)N1+U6)nh^ zl~MrF1}UBqN5v0|b)=sdy&eeD9SjitYm6v)wiAZEgWZ--UPH_^)*{K(zUm$H$zK!s z^`ZRwl1(VUIP#3xG&$sL&I&3Yt9AwC51=B3kwfA!)EVwcDmHrW+6~vc#&i6p(G?g< zhr6v7G|+VbR!bos+3a?h($e=aT-=A8c)ma3MZ|oakWdi+V^ikA+9BlwV}OY^uc9Ir z-RoQV->_ygn#(H^!-#UC_5|eFw7OC)=xK=-7Llfcf)|-9HIBlZ6IxGu-H_wznut^i za%c%E#x&afW~8!W$iEQYn4G@3QdI}!7`S;dA=eexPI*(f#KWhs>R^zf)3^n!j6I%%jOi610#R+&E;EutB7i)A7V#4-w$ zGnI~KnO;8zRF)xLFNN_8SNW0&zf5_rY^o+^V zu>rzjx>%$sri&BxsrXUODxt`7Z=HyeijShJRF_?}9VdTgz9b-gdE=C4yr1(}bU~lu z@Nx|lfOZnOvhm0S9zS7aG;>u92AK+5FQ>5H4ebfOV@6=Ux@6Ntl*Q`zY;|SQrM0Ye zCWhSi(JF#VtjStz9@?z1dBLtqo{yA$ec0*m5ijf?b_N5?oM&B>RZdTL%<^-*4a)?X1dE#%0jZ#Ydh7|61)O5>ORyw;r zkPt1`Kj?%Z@%sIvPB>@^m-`|)f}EO~as(-kUP+jv#SOn|Z~B_F>V8S9V9=f}6OpqR zeugMXy8_nWH_(Ro22E2=j8e*J>?72uoF53+cZEuXkgO|UBy#9R8*@-)|*R*ROYb!15aIfiJ@Fs1qJFtuNV_3mpvWu2!9865P=96(Aig z1XwQu@(NALwnmL-nyKq(-FS?u2HgwcdH}#>l$NtfUimM$k0Ic&;dblQZ@0SjT4*$? zXL;s2TGd8@!STJ|(`m|tv9M?v?n{W5tcMFqR;d?Mqa{7Hup(n2k{JLZU@c zu&hj1CM~|(f_EnuT5gHSyYnmfL(yr-O0wRqCtc27!*X`2QUC@*?L8g345(>%J3x=W3 zSHV1P2>Pv!g}bka8q-S#Ba>uPJ2*wdv!D1F5IW2QnCvCB4`w{$#d&p@MO4sy4gV9q zln+Ho=eY|KAlvyvS>}E73?b|;S)3FAv|=XETv%#VRo zdR2gQh`F|T;ATBWOb!TSD;ARRXoxyA3TAfulw4Cl+zdnxO@zVqFm%;}5ow4013>r) zM~7j5h+sH-gZ<8MNW8tnK4w9wGNfC!)8o^plY&iY5vQ`tN;{=p-2qB2;!9R&sfBDv zst&4eXd+vi=u@$2sz(}g;e(vM4{DKh$RhuzpZNHN@h1Kk^5O;QRX&`^P7LRjXK8KN@IQIMW}nLS<)YX1k`+n08j%}X z$ZL3Y5!Os9Zo$QTPK(uuw6!EJd(*Cc6*CbPNsddo{ZIcy{tdP=$azfub@|w&*}bnxvo|`i?Yl{{ z!HLcBye-Bws{4D@6=-f!&k7yznTnHTBIHi_HvAS06>nc3|NX_~+mkDFgL?b=0jJt}+U;gz!{)hkmU;n*=Oz7vJC$i@Zlpl9UDVi&G3fCrer+N`VJX<2e<8@D8xrDjAJvd9B-hFTqo- zP)=Uxeu=-2)aVBZ$6oY1N5l(OyO&GJ|^^LkxR1{i$a!ZAXIRb9W-)Z?f?w#BU7A#FdB|CW3a%@80Rpv8lq1ke74M6)@MiN7TKym#Cx z04h|tcZ8d1&0BNLCCR{?rn zA-xgl>rJ2%1YMQ1C>J?_)}Tk1_U9elmW~tsi%nWDUoLG^(s{0X>lcj13H&CoN*{V7 zx;tp>sy&viWYa+YxK?k}fcZP1VPKK^)iO-|H+pZYr|!cE=lCaPjYv;fN>^rRhWJjd zKftwDPp=hcPy_a3WyH4d!Tx+>wA&)pp=r5BaZDh--w6+i*WVADoibc^3+p~xL$1f1 z!O9=Y3PnHw-mL+gy@K!LS(BCInG`rw>~@Dttq`Hjwk+Ny&%~Yh>Q0VY*^aLAm{rw) zvE874NJZ$dT0gp2}l7I)w# z0J&VCyilU2>cx1aa~#YVxo&_GxrXpL^xN(bHpQ(l2*XZ)pLqSl{Z1c*3S0CKuFyh$ z{8OuCa~xnCOFG)%uMU|6_*8)Dg%T-TsijVyp%~}?&=ZEjF)#RIHBUBIW?rb zrb{Z%UG*1Qi*!Mi0|CBwY{^fWgF%t4=U{-xItPPVpIhc&aGA~LORMn6TnIYo!tE0b zschIyk(8xLAu79i0;59C`BZrkk!L68uXf?GTE=|ggmJv%P+l#SBqRU7tG91Dux4Q1 zn~7 zqJyS!?cqc-GL=$hx*{`{=29qu5zHck0=HA`-H3#}o~ihicAR$DmNS#Woyyy$(2%Ju zluFf)StN##NdByIvsL?5uiCV!QtiEaq?Dofcf~;+ExQqqnHZ6Fzt`&wdOd&8>$mk6 z1uKgcEu|b|EP&`bH3V6I5DwMF)k&2@w8hQVr6%j8(u7qU8craOnY>}En{&bQiqpuC z=gB-Nfro+?hsiUS7g{I#m`O<`<&61AImx1Q5zA+a{;qE9OsjL9#!Ei0?}^~Q zC`PbjI?#;{DOgOS(s7$mtP2vT8B4fB4Z>H92Zqu*mz+RLgQfxY8wc2-Du>oTo`UFz z?Q!28FUlD&l0Va`ao)@h3SR!ZRnD+$tyaY^Wye)0X;%#h<~NRTS}jnA=!c~=+&}1q z`@|a#269RL?ssIG^06c3j9SDeZQsYo0r*>F>}H8N@OJgBU|CEvo~`D5Ar^v8S$kbc zW_(&f-)vGgW5VmTzzu4U+bUW@T53Aq3NlVITC8L($7yQ4v~>Wj`y|`Qo}3UWXqaDV zST{(N&_0{8QA!^=BG)sP&UFTE$aqn{XgA1@FaD_++D^x~sGkt( z@}llM-tpxg3P37(0m96`wBZH`0c0oVXXH}%XEXoWqRYFI<}bO+O(1_DTgyw0zX?8Y zs|wqeMy=wu)=9mzF6^81*(G0Jsh%VBs-V9Qj@JD{i?D17I}e%i#oN;lZ;oHTZ0T_C zR|TKrePbx^8AIX?hhYc&)v;vdJ9PrFG`?gL!$9Ey3P*ER(wLUiLRx|LM%8}x_VvpT ze>lEGy@d)lmm-C~q=gTEH+}x$)#cmQA5PxBd3E;vhu6m!PH}9OQLD&B_nPL=IHN{E zV&=x@qdSm(`;o2mOaCKVt<`_^^5pX6yG>Q1k>~ep)l?{#A@L5RXwhG{|0)e4 zc)^=QyU*U8Ty)=`UZ}I$X1slOa-sEr@6NAO_F%l{7)UfBU*Q{Y*puJiAyUBWZkdYi zPMHc{v3aFGyQQKZt2Nyzx-{3b+tR32ct^Xggtc6EvNFtY&(iu1a~ESziwLT+UHo42lcECX19L z6Hsy7P;e=M!L=3v$J6$H%SaLMYXoXj0?pbmA?ojwN)7|7d()XGMv@Lx<1E+4Vfe8! z4)b0<=2R&`2G{w# z^yF&sdi~_e7xoc4w>sYtMeco>q)PgCU(zKj=#)8cM`XYEz?Puy;PW&_C(X7;tDLJ- zev`S&zl7@_mi=Rr#r($DJqEp={SYjfNsaqp+b(!4+(s!eBy^wEOe*_5dCtj;iYO&h zTQk5r?-Vb;&`(N&%35-g(qxW#uMZE7I{gFU9UO%!PjMr0NXn)a*kUmTOdfTmR76qH z4CiQ!<-}sF^i*xlAWz)H>T+^ZdFiU%m%Ob%1>XxP6>P^AX*6fu9fuM4hHj1FZ`yvc z?4>Sa&l<;UYNmMyErK(erYxJG2j_}9tl>1K*@ws8unK;o^IV(*o z%au_;UmLy^Hh8=;pk$H7tdR1WOM@4LeadMDL^720lx){E!mLUx$+D$=dmF10zwKwRF8O?Cg2*>kte@X_Y0#{JQ@rDGU~8@SUIkb(Sdig zuA8ZTupxm5PvS@(_cNB2l941l+ilf~9@oNtw1_xkPCTzoOcARH=aY-~V5U*ZakWiR zTO^-@)~@0j@~yCb2O`kEI0lXNRx^9VGl=QxR<$JP8DhO}tm$2J5wrAKP3X0)J80RC zY1Sy4&qS#geUvXoq#gEp^R|{p=8#jcJ?y^8KLU`w$DX^z|(f+9?uzv1wK& zKAlXGEGbt6GP0s^Laitaxyu)&Yv2`%8oBBv<dG#Eaf*O?DhB1tb~~7>fSqMgvLKDgi6092~j}`&W#V;Rn=SY zVcu_&i9sfVs{h+E?C$7Z`KnibxVwewZOw<#Q>i{Kie#CjY|36rAy)~)CZ|Y5b2M@! z#=;U8JRgxCkI&Elgr2E1mcrpB=jE#;Wnv{tHn+}@cU9u9B|UWHOO4$&BbS5l-QWP) z{pKG{5GRCSOwir4!W*JL4ad5D4RS4@GDjAy=N4S?8fi8nY5ueDp2xPP-< zpW+tTK_}5(ZvfV?!^1uK6SSY}OIm3kc7{j8wR;cDp2=4(k8|Zt-({c5f}->O1TG19 zFKH2Y@fOo-pjVKudNmFyb#TnGH3SYb0}hUpo3mrjzkF zINj)iEI)&jm*xpd<><-p#D9fBuPOZNv1r}BuV~$kL7FkbgsJSA_3%t5tsdgfq^kz- z^%}{AU^M^_UZ%NP18IdA{$r`1c zJ0-=}reHuWQRyIVX;D3I6OEN@SIy4zM>hUJ?h5l-=Y~O)4wKHty79CDVLj-Lj*j*Q zZIh7)zdw%SS{f!tV%4(KraIk9W{WW<)&YZ(t;R8GVM@4ymJC!9s@j6ZiD=9~bSd9x zTmYvoIp~JIN}OLMa7s%$mU4^s6c=D3bJ$X4d~9oDRj}S3o!e0j2)0Zwe4#MqGeqBY zNw=K@NQy=cQdEhiI_)py%#jRFX9>z6C=@F+G@7f>bk=LgsSr23h#y;nrel*_v8+ro zCpA{>6h~LnQvN!piPi!+xP$N%ol#*{F0~T<#+uHrR3*)AB4keG3J|QUx8i+mU?<=~ zfy8RcM)FK2yGtf;4K7hBuLr~o?(U{mtc|{E#6@lFAJYgu{u>*yHbJ-qFzm`j!E%yN z0rP}iSlJZ9(k*LA%t|Lym+56W)4s)SCM$}bi{+{cgNa!IVy78SCn9NHiR)_U?SClcYQQi9dpHJx@fCI5nxPq9}_l?IBC zqTmIFBAAg#X(^dy6WbLmi_w+1Dl_|IR?=Y5m^$IB?EE=XC-erb`w6xEZ88_wDP5c7 z{?|$V@{`=JX7wpB1k{~ki#xXcJR*ifbKt`c<$GQbN|UjbH6T_`=94lv#wI==&`h$y6spL@kjbggIY8k6<$VISz74h?-fP9B~DtN92ch?=G(3 z7FuhLGnKTQ1?m%XI(Bmv_Pb-hREe7Go5ub*^cu;T1UL>g6E$j`*z9O-aA5n1vMDQk zi-VlRETRQTvY+r~4`DA2C{(x9bPEEN^Hnqc)c)QfME~v&_d5Nl(1POr-jgn*YYb^ z-2}97>j)+fBcqy{%YwSx13HGSsY}*|;b}bXY7?LZe?tUg;LfZ|-{G-aAc{rTk`Ub; zQxy_1e>#u~z%7z&s`QYq@=)&PIz|G)^a|^blQ>;_nkEx=0uZb&QSgyG236Yk1&tbL zP4(lq&f|}a<;RjEyr!QnjiY75OIZ-+%J4r6NaC&wP^s&|LLU}wYqQS-)E8273dfv@ z?AubFylE08rGjs#nM*8Dz87$)FT`4HX7f-{QQ;wXK-U%Tpf~9BkLuiE6D%K%m-22S zSbPELUHA6(2Gxz#xYK{loZ|7@{FlWo$X9Ohb8!pBs5P6A1OM*)3UNr>fi_<}Xna$Z zO$cl>&gH!q^!y^)aC7&IX!!WsoTRetjV|1Osv8|r`W4*h!UOsCeLgK{%*Y#FGV%O* zt7TK+&goCQAmf73kCF>4@XLVw_&S-YV4~?FiSN#EBd4P~*M+*vzF%^GIq2@9|2m{M zb8LZNvzyVP3|g(T5o&V@VOG58oxs}`20EgJHw_kI)YfIf7~5`jAh{qIxeyS z=tWeZ5JCk3Bwj_;F`-F{2gPIoi7I6o6BG!?t4ibrh@o)UDgxh($Y{*@e6*&`bJVeB zcRVLCkHm=l%S+-JTLgtMp_auBhQ(MklRA) zC|!|dU!wtvRaVkZHs<_+40O?ftDfZ7#z&`1VBOmxZum6~4OQ07afj{(GP9|XM)0S!f4 zi2-9)(t(N}SPrapNz1>gsY=~P1}(oHa$Pp{CQldB1hbZ_U#$uyW^=AGYxa&h2gDnM zJ(b|t<~*xq8&uDJ&YY2G>%*Lp@CgcuPnbm#hNO?0!Tu3OY*vQkVfp3Mt9g>e%8heG z!rol0-FAd(;kil%x<$sZK%4L4Gxe=-lM~rXs!4{brSyB`dcy>H2Z$}&!<{maj@SR zgv1*(VH#Q_u}jQaT){mvAMDhJ!CDn;-(OVANTM1PTT^vG_L6LpE383n;!akhMVy0VT$Z_Y%X`sY#JR=ovbl~H zao$APIG*A`1EPbeA`w8{A?^aP8JAV!K6XeKOfKRCpa>wM?A}!v3MN*pxhCVt`V5p( zASKtw#ieUpCN~i@eydv&H13Zzg70R`Ao*eUl@p!2bn=CmH7y-mSJ-oL`{eg$jqrfD zN-IDif;=-zv7}Q0iFs+7>I#FcLHk&?1V|hOgnkF0U(>d9)JpZOS!u*eYfx<3og1jD z+~$^Wt;{A)ax2jdoW#GIlaLB0=|2KK*393szXf<~g`4av=-6RrZ$P}gaKAGg{DSx_ zcU?jnnXU3;srrtbATSbd8m$KY+O|WoHwX{g_gJJZv4&N7_xjO`(k1lpMd(tgkLwy4 zrNbv=?<2q3k)60t!t8aJOhKBD6=iOxpxSp)iyi=7%#qm{lvl}^w z=>sRrHl?g0t*V1Nzbmu=asF|*Ol6ohfs7YfoU*{78qpDu?3TP6(I6_y+pAoUHwfIk zj!vTVjuE?MxsPn6=OnSop(-r_*jn9|3FiT1{JXt1HbP;$W&k$E?d^c9rMelCFr-R# zTX8n|xuDE#GaAAf*0wC_(#>i9h(+ma8rz6zkw>ahk3dA}Jv>_1D^o40`mR>2`VF_s zP4b&<*M?K_R+4q0tt!z_=$n!EOnU``ky?t?_f8?}WND!tQ4}lkSSJQwPOo+SBq#`UP(3 z&n>Te)gk}7zrXJ9pJjhr&AXE8stJ#zyy9r@-zs-tEQL@E{k_gGB;GI_cJ`{;cF5B4 zuAmV(A=hD&?uN}Nv1nKJW0|mNJx6m`l1&s~CXW2^qU09^pCqZ% zC@CSveA-wo(l%9(4#WPwPSZYKOe^`n(Z>6ZPEnQY`bJ;Tc@7!L?f2;Xttu7j{S_>_ z_m}4lymfeRaM;x!R*RvrDdL$Zt1m8SLFXe92EA=Mw|_WLgF9te*gyO(DJvb!3!c-d zGTmYO@daoAR2DOIqQu0Ii!m!;IWa#`seYBk=@@N;m&|6AsGtjIHfpo!nlr=0jnYTwEh80 zsVEZ-S}Sf@sfT^tYz#zv2l21A1$dit5KiEor}>WYqtNDH_2NFZ;F_7V>E^wi$HfM2 z=|)g?H{Zs;Uevx`)V>OGzZpU9>qYJBMeXbU{_XB>b%FXGge{HAzX>R^0wx;ECa4%1}3dB0N2bF+PB3&=TTL*pH!n&I zyI&N{iPxR`&pf$hF1R@Wblgg3X)O-Yo!DQcGVf*@y3SO+=h z{No}gzD)|HEC0j5#1a1yQ=HDnNycK8<8n+Ri(MO!l)R!6%wEbxXR_Ik9hAi^)?p<~ zkoiJDGEh>_lZ;To5Jci^1-&cfFp8W-fg7OmhR(G-sZ?ohKe3y&5bU!H>j&fn=&5ox zf2+b+#dXTG(S1{VH-v=u_dESQ@%H;ioqn$o)Ab+tf?WOZ_Wk)OdGq$2&Lkt5iv{w* zd@|NQ)!DhiL{XeoVI|d;O;oy5z9=D>Z^ajdUI86lR1Uof0hHDGD&!lEE5%3jy+qF& z23k~)5YJ`*-e%@+WNVcNhnOr&$Y*h6Th3{sqT`Qxz23tCac$N&eQRTfg|DQ=K*MoR_BQ!A%^&GfNN3$}^0rl>!)K?#Hka>LRza7Wu7&SpsM>H8~4 zmVh$@^LgmZ=ger_-KPdt#CcU&dDanT)55crR@TyC&mFN-lO7mBI@h)z+~(BA(NL~( z#hzUNP9{13PB$3|m`Z>V411lhN4))ESiRtw75QE;$mEQx5PZWnA;*I7s_*$MHk>-i z2-OtNIgP)gDa|4U>VhjaW=eD3?;ZAj!}~#&mOX_O+y&m4g?@1y$4nkSq#yKAW{g0L z{s_3FbDGIjP^f}}EEDRiYRhmpV*5Ac5GZUra6ud#bixDT?H}xQ!oB*6P@BW10&zEo z^R&J>l7!Ts@dCvBv=O~D_WyWBdeD}pO!})!A9#H zzX~>vU=w~7Y}VGi#?3sxA_~U70kK|T`k!2$0y5_IVC`ipwB8Rvro%mXe>?0AG0Isu z^8F5kdYoM>_bPwwD<2dzsMeGTCXyO2ewp(L+<%Odi*;sLqPmug4$yg@)dJeW)c8j|Kd^xd1K)3xq; zHy_ZFhTF4I5jY4z>e?CHa(e|snryngqgSuoTBA_C<0k@fwslZ5tTXrS?nL*g3&cXO z!k?gbLBUfdS{2dfR>VtW)IMF5vkd+S+PN4VlZcDfxE`dF1k}DLDtB z#Z>503WCBq7_OxTDx+M;!_X*k8(Rc{>oI80%!g_nFGHQ$8kVp69XW_vAC-LAj=mUR zbgdbLPm}-wD!Zz%fqs#Yb!Q~IMv@G3(2K=%%7jkAY|r;+J4Z+RyUmCkbDC)E(b@-j z!RK;lf66#7Vv+nLH=)Vo&C)qJnbD$jDBP`{hRtR^i&-vq*CrYBas45&XR;7yP}u1%L0iqtV3V&~SZFn!aC(ito3(Kz;=(K3ouodrb@C z2{m57={f!-XuJkPIgtl@M|U#wM`>|>qx1M>wKxZRL+~WqAD|Y;^(5QIcZ_W1JLX&8 zvA5cF%zLCinGVV)J~@Gx!+wZeIs)go!*K83iYdQA{j`Pt$@{WeE7kH6qIA^V_w2&G z-fnQP>jnFdz25eM{jb;Crt9s%5Bq*^SiRorw5U=vL5D1LUt_VlXvlk;>qs0-i#)2( zh*^|M27qWei#Ufp+Chz&;|?tadWF1)*UP{B>woyq&eb5udF=W}>GZC!u5zAIi^N*y zY3-7#?oqBIj!xw!4-5;zf{0{rFLcY}1x)t8q(!-q$H$o2Z{nCtl9U+*pM{x1cd11t zk27__Pv+3>F)MEv%gkL)?lurWA&2r7_tFR4FJ&1$_jVvQ1YFvXqAvj79J#~V;@=1j~i#Esk$Hn$`ZQ9<)0o9?zW;JRA& zf~c&^pqzf2F_3uW9Vbz-Mal~@O+K16(Zx)G-~Q7->C%j}^?57bwMjt!yHNROPq+)UEC+U(Y)-ffhgi)C z%(r=U9V6W~8F|qxRts^dMgHAR!K>!&S+1QJDQv4;;9TCV zBc8>|w=&VU0E9F{I1Itam`yk$Pz%YV!2kp@?Sv-UKB92CuaMq|^jlKUGcr24LjF&n zHBgOl${5{dCn>4;D7xl~@8>-BO-bju?yYYyx_ouy_|O~C-ND4^wa2oRY#PWP*X|jm z$}j zlgm?g0iDa++PX_7I6d356;{b!h|agePJhtkeM>57sqtXdT9t}foy)?o&PKZ_hlXLdA5=k{>NfE~T;v(x0Q*PBBe;r~(0a=lb+utQ&lXS(VAN ze>i@9u2OP$$dp^QZa^Y-$un_xY_A!peyA#f@cOe^m z%N%uN$fMvdh(DG0DFJZAp)7V9}X@C=H z*HGBEET&5-kA`jBQy_b1#u1XoBCy7+WKl^XUKA`U(-m0`jBMv5{1h`vl8js%iSk+# zaIckDWkE|eT?GYC(9pep9H$5Tz?LJpliE&9g*Ovf)DU8q%x-IjTM6;QXy$Y4!D6u0G=E!Z8F0 zo<=j8rc54acq5T)&POQowHtE5Zg_#0;H_@#7LFN#TwW!gNV&?n93@xZscu1%Us_wS zT5zy0$jvM%6@6M--Q=zCw!nKHaQ_}|D1+b1T`M==B1_7Wi4y+s^AS)|W(uq$7AlOCJCV^(%tcS;=woBx7CpWE2K+F_}+j zF_n#L`=Z?;ZGR#EUBtOxuxL3t4EsZPkR_jd!LqpIqpoTR{%HTnl0s?bwUkWkB^G5Z zY-229Eako=D9b1WVD;X6x^eP0jgoiQ7L7VN1 z1sveqtveB7p}bus^-Gq5*$Wd)Xe~N_I?LTB;ZZ*T^{l*$YaG;!uuqVE&qid>BAT&k(M*JaSVn;gqw!-tr%4vn zilUY!h%*r&l|U>eljM_rZ*1Q#Wtf#L=B04AAjowi(nd&)>c=e3L=We%(p7KEj(XK@ zdRunVyS0m6rGs8m_q>MAc}-nwI@siXy;!8_l}fXq_ptM<>FA!XF4;5@WwH7_Tj6(d ztybB@rjK+xz;u&LShR`&1kX5BnTPK3%L{f@^4ytaR4F^N>oAz^ILW#$mk}vrIoa#k zBO~Xc1ZBEIL-NP(S=l6h?zu&-ut%}n}vZFDdRQ%%?>8%Y;zx=3l^G)EWHrK+K5XyVu*plw2* zF`bYGwdMA_Rte1%xeBPGreZ`b{Kltw5{P6ff{%xw9krfVU6He>yQ4k~GCa1EY^pz9 zhH~1&;8nrrMkFvWou*a#U_VY%6R&Oaw3#^|WQvkcJK_av+s7Xh$!yxlsoOZ!LvSmX zKl*)Vx55x9imrLYO$dV?cGgdJes2Z;4w{#weo|t$8inUDGWN43!?PdoloR$MM6~C2ti9*mw>$zG9Iw zn|G!@P!&xpP=QjH#yEvph@V}<`EkAL)TEtdB}q`7vew?l5& zxMMw|c1>C!7wtDGzpd%(ow_>;^hIHHD|b`vJnGm1$&cC#rTwQZ=F3^~&ZN;rIb&I= zoQ2g0)pv*Y@ZI6LyQ5W8{f}yQgXGVw{sp;otpkJ?3^^{V3%st>^JvQ8w~Dvw1i-x?GLnz@^5xt)mNV{!B&57 zAZ4Ha@Cap}rK8?#ZBVG9(g1|KIMzmd6SA(=JFPXPsZutm{_8x$$`Bnf*GxzO_j;D( zIV+GCIgsMMdnwau9{Z-MtXR^boARkv2Pxm92i5CssMWix&Xz^-Ht^su_y@gCf1h~$ z!+rU)hJwCBUg=ZQS(=+7YN~z*Ds)_u>+VddWbjX6@1o>(#C7vc6Xv>kxe;~!7Rugc zy51(LstZqsxo(Q6Jtsd^uTs@uFeLVp^a8on(fb$t90Y0^r1eoKQ*Ts>r0`M*X{b)Rc(?tF+7s_Nijv z+#l##mG`Nfs0QHts=HIW0;n@=+gcxlx?ZWg2EC4in}FBdDs#APxK{^FULnyWJah2l zweW7qLwR%XSmO6XQdiV&#Zli(c@v7drP>Hzy_w)91a(Vr146iip8nt!WuVjVT1*wx z^dno{84x)A?pssTiBnv}xxA6c<9gSb*hQS{Nwu+yi#T7W3X5(`g}UQ{kYdOu0w_C# z?NZU@9M~e4Y{4YMg6L0l{<$Z4Q(miQ8f#govxz*zygo@eTN?{Yb74-hMq(OsX**(d zgr-OAd%Z;Nz2zO5S?fZ~ax3Xn?8?0Rb968C}oHrK2{3~Bj z; z<27cbPujg8Y*+stRQ?Wwe&z3B&}%m`=BI<)L+VCdp)0R3WdaV@BZ4k>ra_+@Fbr#I z#Fquh^szAf`VLUxQeovVG&*zzlfx=k`uCUT2&`ok!*T#*cl6p5XHotrhNg(G$j+I&ce`~`zhekG|h;PfMB=e-G~NJ zQQlVFQzNIDRzTQ~ZpVc4Kt8#(B~J#UrYrf#R<;R}>_@h8TTo8}`{e-DbR!z@)N0!j zQ-K+QvR&NMseF%`zE&Vzp!sUTd56WNXW;rYUo( ze6X%e)2M388YtgeUuMU!qA_bC{ZO5mt)fY5)>PAWdNWJ6qB(0KxSj6I*-)-#dD?ulwuk{?eal4cvn_*Crj@ZTl+h4}ac8_*Dx05^Jlbn`>BN z575r7Uoz_Yx$URHCALXNx7Dz_gdRT3E|o@IolE9^+B#jbNndB{hgXr2AFahRx05wJ-g;NRv?kBAtx1>X zH17*&^Nb9ZRu;=sYV<7C2A$rmRX(*=&uK}sUhnqSJeg+CR@124tF+?h)9%@3Y}D^n z#D&kI;nOuZIzCJld4DY*lv0j=Cc$0IVi+vjIGqZ-l91x6l2k~Ay490aH+gaI9rQZG zA@TMO_vO!O(lQ7@jKKZ($rG6Q4dO*(3bV=pn}X{Wmd&y%V`^$68i!kbT?dS~; z4i3Be!+Jq5>rnj=Uo8FD-bV{cTqwtzOVJQ`5 z64_OL%j!Pt+fHE6**nzsIBh{T`qD-Mp$29v_T^QXm)9s*-paT?(#8*eLS<2A7w?-oX*gs<1=G>UzVy1BI?@mC3ei6`)Wvl4MDXM1{B1#_OFKt8tRiV$}jIG>!lo$S&TV zet2{I`XxpTK{kt51)rM`B#7y{WD_SQ2_9ik-<*|HvQo(E-$eH6;`rn#XhW2!9QazR zy$W5gO4Ylgy=&yKwU42R?Q|kBuin0X`QZ=8mza83CGQb@&io}UeE7TR^AE2s-@g8E z^7hTEv+qB=KE6QcuU@ZnK)n5MzYz=W7Ykda0$nr@q7KWN1s%*Egk zHthaTZ`kT&(0Iv;GBFrldwY9>YO63kz=K+b1G?TYorkEDR9Sp>QAz9M$Yzz~80&P* znAQlH=XFZ!9W%g+j9e7_(~4*$G}S|SUV*MbCya13->SiodD8%>J+N~%B9HCa6@%Qp zXis9!HZfn^p-5=lEM=-t#`=ey5HR5m%5B#|&dz`m;OsucFR)&?qlipW_M z*whh_x<`AaU7ZR{%V82}y~o8PLL`eh}^-N=??p;}K#{)*0% zRD*}RbW{ZzV*TubOysADNvCplvD_mxi@Ur4bh%v&_&qp00${qHf*Q!B+$BnunSF3p z)*%kSodA`yRtqcSvS#F3zNAxj4XMpp0qE=LQYEonPnzZC_1uxenh3dNPg0u99S4Q= z=klftDF$9cg!m<+@gIw%WN$M>0TIcc@Sj=-P^XgT34HbE|8n0f7 z*HncWNPxqHi=D#LDidF_p)4sOD}1EvLL!>6c#&!cfYQb65Q-Yl09aKhH<*kQj@X;d zr)XJI#NxnWPwZ{Rp4fXl_Qc*-?1^UViQb_f_8f%5|L~v5cb6}Ze}DGodvfx_@tf~o z%4u~nT;UF~Q;O(<%J(8lvT49`mX!v)uuLK2fw7%HPL(uOC<3}HA{lA`gmHoHT>wB` z5iW^f7R_+JNXx{@VR!l6@d=sJESboyW77DS@-Lmqf1*JRc%~bSmgzge0rzeJD0nc~ zK>zb22ORAkjOlPc2o!|CW03>;hX50G*a-&>zyT#I=1B$%@cV*B?4k-~wco3tjm?uR znJ?xIsAF1-kTDkPyZ$1FwO1@MNF}DQ$0T<;rEJc#D^_+-Eu^4i=mk$D&(ip0V0bji z;9HQ*ER$!Kh2ZKbv)^0SQm-Mzx&}fFR{k3HI5Kscq{LKoE}sCRb<6QKs=J$6Ly(y6jB0|ZLR6(NLv z1B7D>@o6*91?z3Oa&oL*-2oQaoS_hPu|C%*nhI6ILOde8#NF76PXJ4a8DINSY74_7 zl-kx|6~73oq5@SU4}n$mnjStpiz?Y*mmco#b;2R>h6iD1e_eCHvvg&3E?kuI2BU8n zVXqg+vq{PJPq!fz!AI;!Md7wBsWDBOH>JOAueR8iM&)$J#%yR&b!+;1Cbj`jw-88y zyj9Cr=g#$w@Iq62qrmp{M5E+?02%xa4~Dpfw9JV+SBjA~c^~4W$s>cOtWewmvlXq0 zruq*QHqptAmeCBxiYgsnY3bnY5vc!zQWh=*BPGvy%BL$Layb`hdn%~T{;7@@l}qjc z1!1}0kP9Ob5B&at6xILR|NTGy_y79u$@_d-(AYBO(*|^`0ienOu@M;+90L!m zPzzI8L5{0JZn;o#==P*ZR|@GSIG)?UFO=p&8AT0LoJu_lDTWc`QOx)vjpetIO7;q8 z&a<7v{k`2aKs9|&0DY=4)!XAy!R$LmeM3Uy+yVQzD3WE8vMGBhB8q6|6#(*_#~+}1 zR5+Q`h$|5{UNR&jm`mA|M$rO}9x4PYN_13a6Ds-Iw=qe?n?;&d?;qPEpY1*EWh4v+ zpZ_v4*c*<=@nI0CTghWDBVlg{3MBZcOcM=h`fEA)n-bS1LiJl|sI23m#(wZ|Y5K5i zAC%W$=ePeN+4Q%U4u3A;aBm+Fhxhge%EtGFd={HRA{w23>OPBic$9C%TCMmjlDizW z);@3@fIz6m?8n3YK+4v)*v};$u-~VA8l-H=(if*MzkC0E`;nG)AL6@y{8P&V6T8=7 zV)trhaIbC#_wF>YH;^?(>qpZ5tfuSX{s5*2B;2(t&zdxB_mY$&uYP#@{`{1@dHYUD z+BJaWqLgQd4Xtx7tM*=UhFDrR6ayGk3ge_ClY-9)UzG9!P57iDXm+$=aJ$mgqN)?} z;JcR6QdFkUSYH`jucc@ZX>VwCIZ52S-`Lx#DN?J6^qs>V`jUZa%yAg=gAm62aM(2F ziVPIDLFeyIF1qheFBC|F4KIIpav>kxu(3A8R;|2=aBQJ~IctOi`&_y1Q)8EmITP8p zr3w340iUOtODieSwh4jSaGR)BX0bS)zCG!~8l+tSgt-gbOz`N^a47G)&K3W{KCL)< z>^05Yn(><4V!Srj-mmla6!T`iINW)YPhsBn_hH@+dk^4pbGicj3e962R#&+{d}Vd( ztnToz2i`b`M|*lpw%af_`G{@}CyrZ#>>yH_RPikxZB)2P z8x=lY8x?-lMs3hW?fbp`maAT>*-K$ws;VWy$j3urE}wFJ&2ma;p6F+l@2f^(G}x*e zn*I*Ba7l|PgY)Scu33!6a;aYD@PudeusWko`al0Sja#p8@;@xbWEmQpfvXVG$|p*0 zXaNRDSkD!p84`1OHp8wk1tUJ6^Gq9@wEf$7m{^SyXtV`lPvI89BY({;s-mPami23z z=i2-48m~T1=aH2nxc^AG&M74S%a_NeuU~>MEGUU$9tjeq$>&!G9fbXZK@m*CMsXS3G`N3ffnBiuq;bY=IXdojE}+QMq5_>7tIe=Q>o@-A^uIo|*Ai1>_RGRs%CzjQw(v zrPn4JrbgC@x3x)j%Gp}3hgz|Xm{PJmzptZaS%|9!DklYZP>iePPcBc_$^ys8EdIu` z6YZ+Gt9NF{i4JxaD2{s*SfsNlT`fCqO%)tjF0zcJ%Hnv`3spIJygJ@E0XfUGgeu5< zQ7&kjt_b@irCoV8D;w!bd2%UqH&sPyz1r%xG41R|N2J|#V)1ik-QF-b-g(W6vho2$ zt=sn>*$Q8K+N|#=zL34@xEae_J>^;0oaH@!FLmG%Q9|Hin=fQ3`});cZ=nC6YoEUB(@BP@=*`g#dzp+Qf7prh9|1KDVUx0I$_e=LP!^QHnZcyGA^RBw3 z^$&Hey1nrarCNmml|X90ldU%!ot~S9WzNLRjsdBlUl9=%G*7Z=W1}BU6LfHK-TA%V zbU>TmJSg(@R%84iI@rYX%>!%Q=_~!0LL1?eiM-Rm9S0X%bIa~c1C4O1_=zd)YK|+B zeJV75_V0p-&$}^OvQ%yZ(T({Ov*~pgS@P+F0LO{$zb)zb1pvrvrJb?|2_DhXhxjY4 zn8e!!p=q9EtP3BE!k`xn^#`<=0!*(j00e&_|6RnnU$AJ2Ihy5zEcxV1W?k}8SG59v zSlE9TlheWZ*KLbMj3rtQbiWH$Y2WUwLwyR%#;U1mk}N6h2m`}x8hR(%Y~z5KPaZ!c zhE#P>Jlpg2VR2qI4ot(se4??bEZ09cz^k?8W!q}ZAE1$!&9sk9qOT<_y2T>v;tKY4 z>c+Jht~JwcF{4XNQs!s`2^B=l_)Ufi*~TsVXqs@DaA~*$BG2+R4!-YbD%()R(H%Q! zu%t+{;tS9VCt1nexi+} z@C$Rptz763h#Stg(>GU2%Mg&`JWp40oUi4oe1mBJcqO_v)jy;x^gPZ)EhO?)Qi$@8 zNjdugi^%WdOyG@9yMAlW9Zck)BAJi+00@3?s2olj=S2^H7V}cL3wF{w!E{UL;tXIG$3Bs3p*hOB5ooIki^H9K(FaxfB09> z87Kc6UM|lZKeUQA5<9s4_Yd`GldV#kjApMj@1AaL#JIKHKR93;j z|DKlYhOV}yt3Jn(KDj*IQ~@E=3gKqb1)DBXS~$(o#afD$HzMad0lm%!nxfsS9r1!W ztOxZ&ZQT0KbA<*lUkUX_$$j4$u;GsGEx9pnx3Ka|G`-bu%CnOeS|cP+aJ@WK3Scz= zpM$#UlJ*!-Ms*0x&{ZTEp&2SC&7o$~VW)l3)VjAf-f*0hrHjt#LL9b9MjQ#TXHyYnk; ztx^>5G}Mo)4%MWG#~HZS@5o|OEVsgR7TvY;iKu0Ta!$(vApKdRl9)+vn0 zB~Q~No4&_b4_=VB*(pmIZo3i^C00HG!;Jf2r1xi45bl}BSBqi_l5w>wIyez^<2LNF z;A5skVp{gag?v7$XyL8)J>p<51os~m*(S`EQ~GwTOV??xu_7va0@FBB<83#FrW}d$r&Q`;bf6wJ|q0|0b>MvP7%fEQo)*LCn;;wQYLf87o{_1 ze#@+%pJ`zXzAlVk;=)*SL(vWtLcXzojjETslgm?AW`)RY$HrNanv6?EgTU$>MTpT* zDG9dY2`g9@xe14o%u%4J6gM*p{;Nr$Zl(>+BDHL7w$4g5uA7R%Bau~}t%+D0-CAz1 zsazBp8TJNf%P%2U4~C&>uNzUdUFw?VdAX`rp=6MXHbS3qzV>?2UsZhNMugeuqZiiA zyEA9yjK?pW^clJnMM2>VHKVVVWEsr@ayB6;o0NpfW_HXy65b*camp#-$v&_eHq^1T zm1x#(RTrL`h0nA2?zIrUu7NMN2Gpao3w!@rpKv>Lm?tOfcftYj`r&~}l~ZMANHVA8 zOy_{me)*Ci9>=kskN=4s%#N<9HFol~Hej}E<0DB_Uh zG=aQ|R3-kM(;O8x@`MrQaZvJk3br61dps%CNu`=Gqk@d(DG9_`+yO^A<%~W_rUGp_ zx)0ZC!xx*;6$nxu&+Q4Q1~Yzm;#O<|LA#F_|`h$L_4=U~&#d=ZbP8;8pIs^M8nZBlZOD)l=J@o%&@7p;ml4{3}*>x)kBuR`wBo_ds(#%eTuN)4C&->kY?JxGq z;dO-X?cd`+;Qa|-Z8olR1Hy6S(dfm&<6vK9x zJZVRAu0x15V3PizE^h5@#~m)#H>U$CR{uk;F_~isU*&>=T%Se6`(8Ly+?05SoA5qFjAK(Az6gKcjYZU#4NFo83)C{1f`I2YSWIh*Xinb9d&lmL^ zTR|ww5I(5t_Ei#QnZmr;};fVq8lEaa(1blPeLv|(H5B#(E9Bb9d=8$O6a5oeza z2f6B;zG6{;SbvPyg4S<%Ee$BY`+KQ3Y61Ahg!+Vw24;>lype`1u@(&@6Rc-!A!`kM zidqhBg4?xK$O47guv{uNmfqK)>y9=sl+;B!yjjn)M<*;^M^QcJth@aQ=F)pV1mP0& zK{IFG<*@d~IG~7x%^+n~4raO!OJEhTesz9WE z60UIfsQ>9t>@VfN-WlqNb5+vot{Q}52@q``U& z+M=?jVr5^&Uu9PHEaOnSg$TlVrb{00=5e5T9Vi}-!MlP7LZh98SxPUYWCm#&QFuW% z4vkLI9#wjQ8URpux)fG zAElx%J2y%FUkVO8N)^aQiK^jgQW|$7JdBaICV_E;1xGqMW~f!_}!_j z6YByHM#dx^((}OA0O>%XpFd1DhOpHz1N)<_A{W-(NPm>D4){f zDsQqd0!F!m?J%5y9+okLrrDS}5Xhntuc7K^zzhYE7WUygTeOeSoL&YER1S2Q-9BI| zhN88PC>v*GZ9rxGeOhD}{8AY0S_FCYA!fX^jXXw+^jh{RYQ{!PYv_b^y-qHIO+ran z-p2#5jRu_##mpWTp74v9H2(%C@pfp!EtA-sx@8ip>;BEn;+FAB-L~&MaN{Y@8zm*H zL7g|P^xhRffPhk?Q_LZp?Z%#lZo_pM+SNNM}l2rg;dqwYbZ{;ks8q{yv z{?*^Iqf8lsVDo4qTF#;{zAzcRV8~Rt62V5rq6|z9DxGfs>TfLxJ;TFDI!9{mQC8q0 z&sAjVws@}YyA&~cyKfPsamnSMU{4=wS%~Y5qOf_*My@io@>D#y#r-yS@AcVne2#{drF*w!I@ACW}tN0wV2Xs*(Y?R7;y_6xa`OKow&oJM5g?(9PK-xrP*0QB<;ifL z*ppFfVR_b%IXv&ov*)r(TCIREJ8B!mc}$JE(qs^JmvGI`nF$L zsME@@p|VY-Lq;j}f&0Epf-s%~v7N~;AR_%y4=~fmCCqf~k#Dhsb#f_=yQ5w$Z!@ne zQ&Knu_UBC*;Bh5_Z6>oGzhILYbIz({vt<30Pr26@q6S)}28>t{hZ4F)G9GuEUQY>m zOeT9tkVe|6!OWPbYbb@*w5Elwhn-f}mVhOw1BV*I6Dg02wnVBfo6TScUMgi`4^|AP zRtRQqLP+irv$(OVLtPts{zLd#1X?rv@EwKT&$aadMeB|of~S}}tRPKe%jin_E183k z${6GNX9vf~pGALvIy|;=MRx3&Y3DIJ2Xy1ATe(?}`O5Y-w1LfH*Nr2(CBn(eY+)Yr z>F_u&y|9Y^Be@Q8b;$$@P|>QyMS8-g-2t4>e!3Q;{)9Qz(e@r!-cx+;*}?Ih9&Nqk zogck%Pw~;84yzw+@Uyp<*K2U>I|1eG)ZWc7glf?UwWhDjFA${LWQbzR>hn7WX;vvX zTSZy>i%jH-MwN*#Wcw&Q3Dy8ua}WZbtHy)WfvDeGCzXca!q zxPQ@P$B^}LxaJWZkO@1%N>FnUqm@H%r1Xzrm$-JyIaMuobbQ$>{58M_)0VOyo9qmn zrxyIYfw(@^iCac$_qz?>^b{OfJmE``0f=6j6ZM|v%IltKz`O;n{RU?n-@H3+j#*>a z>u(N}y~1#`J^-EwSMT{Uj6xVWtga+fZ>pa`o&6n_Z&1eE_c96psAfcTEQ9@ZCR3kB z$^e@FLHL;i$$X5{@i3lS<^FeTpu${KU>F;YH2taApF_TrR>#0XRJ&& zN;z@uW2+6MUkgiWC6W|;kiH;=;8-8Y7}n{ixf3oy3rfTcJqnX{y9m@0NIz|_Q<1dh z>o5>)({9$HYNfVnAkSFUnWHHvrb>0SG{sb{NrIiLlYB1DSs26V6Oqd364J4)j&DGH z-{Ykkn0da-Yc)aSukxaNHcR#&Fc5K~fMAObP6> zp%TiGCpBVWr9P6br&L>3ryL4XI+(7fss+8Ul5DZW4~)yee48I1a-a2Ro#s>^nMt(V zN-2Ovq2}g;1=wWf%^oUi3m!!R$aTnW@Mq55>>cheMH?xeI#@b7>Wry$bW1;I$SinT zB;x_0Aa;NL`7-)$`c@h|^x5F?4pcnB44c`mE2ZaS+HSIrQ)B&5bq#Q3#B^ou>_a&} zLPJ=Kv`U~Bjjdy%sRqD*0Id(!P^*t*2r`NQn>}qRlhk@PC4uh|fC(;+f9*Dvi~zAY z3tO6o91%o)xm=1^DMRWOtsW_>C4>P7jahd?$g!-ugnFG#ePKcYjoOSWNpxTB^!W{r{8r?f}YvD~##u>j}Uu~fIi*V!V5 z|NgP9nrQFA8a;#b=N8BX8|CwPB9sMtCP4|{!7_|#`!@j}nLG22bN(;XYcWeLO~ZMt z>|9!(w6(DB`>>#0hMd`Akx7NHOq%BNKGo(lBVD~>lL0T4Wi@bm2Cc?vp5mWL)ZDms zdaFyUM8W;zBkhlRwDxo}^yU+Lbrr4A=)WRl2ndO>cdv945sm?`l1S zD8j+I8*Lxj74bNW?kfxrX26t$igC#Y6Ln&l-O{%Wo4#7<|43eBhs z3eBiDMBTrl4SiD=ZlXijOVw1r^Fvstu?>L_a6VS-8Z!xR4t6tcQdkhQT4zPzua;7P zAkLGL>oArYO2)blJg~Z@z%gQeoK9eC59tT09r+Fct2N{#Y@j&9lWFUw4H`pTY0d1N zMxg=LgDl+F$MO61U8u(z^~aj;x_n>$KfYQyW$Z^R=bx~z{`4p25Sg9x5i4_A+b_ZAZlzU&?r{;kjCYT=Fo+zTCuN+S+5KxwrNDd(FWI z&%(#dT4@WH9P(HhKrwZf^OHy@_)9f2CX&Kwy1F!Ki8rl%CGVV|-{ zZlRjpyX$PhE7gkeDNcD7rn3-X2oL2)nVr%bYCv#=oPm^2O=j!BDf)vc9R)2pz_VPc zg$5uzK{?Z}VqUTGozxJee;RXQQNkKdJiPhRX6EmOx&Kw{brvv z#;)50wHU7b5noyF$43-YR|YGe&4izo@Bq@azfeLrFP8D2Lzm8Tq=1ukB+~Pmjh>(5 z)@d*+hG+$6X7QJF36V--GRSrvomf2e@S%5cG@!www`+>!eF2Kn<{Mcw;~mHSg=YNO zX~ugkZ&cHC-~ZqL{a^q8|L_0EKCR{n5Auq{LttcP8HZ$el4vdeZd>T#?=Hrz->vh@ zciaBmfFAz4lXv^SJ^J_?cJTfF$KSl07)P&?A0|ej3JI&xnqbXiwtsxYU^Aod=sDeA zzT?mH9sBA#-ec;OSpV^pCR6Ui0t_inc@PZt8PXG^vrZXbg`oSS?9JY&J9u0BnoSeI zF94C`U;p`k`9E}vFklcprofvGoiL-us~k0B$K7?*s3v|HHQPr`PmLO-%c!sRKmR|B z7z@^Mn89JCmB}cK=Y`Q?`=lfM>Zas@MT_Jcj z$rXm&a8)*|IZu8x%v&{Na*!OZG*<1W|C;#=NDlqifBp~uCyT|EviQ~e*S^+iQ>HeL zrRxKXD7eP3i)zp7jt2GZZ#!Xz9SQS9~n^qmIsW<=3#=;lfPS-O&({Vh>J;`VRRn$y@1^ZdF$T#N*RjNL*oxFdNW6g>~z=7=@=LvXuq)I2|| zxSrl;hvIr&V)uAuxsOu~Pj@|5?K*sc(NKMXL8o)?FR;BbddnBMPi6FTeS(L6gK~|s zG2T4K{Y4G^?yZPF+2a$o zH|lr}S&lB~jK6%}XaBz7?fch%{vZB@9q9`A{Oh95dafz;4bqJ90lx_|_}1Za#r;ed z;cSkMxA)qW#AqoNeAdirh5=Yc=u z1IMBLY}akfZT4BiZ7S=q#!6i;^HeLzgYl!k-h>4pVJDOr$JHOjiX(tAh^-GvUA={Bs9dde~R|0oi|;`s~98Opn4YDux%@)v(_;- z*s>yqb-2ayJA@Wd8(9R&I>xMZ>)8}C zk(QdTE%3JJ_PXOi*_zyZY29nGMkj2#POqo(^Tc(!{rn_B15eoR>*=E2Wc8M>{+r3Q zzbL1rvtR$3`HQ6tSnK4H4LTjV;A=dgz7oU_C-C3$-SxlAP3lsYlt|JnKYgE0{X5+y zyGNA|-AHn1M^BkPXS3=Q*!Ww8I*|N$k*5aEf-|T$#$p7evQpXD zqvvZhYK^LdjmD!VY&0G_VWaVQ2^)>dgpIo;5mb(o>Y-H`b8l7V%WU+gMFH|1!s|wZ zcDMTGM>}g+qhB7GaZk%h)1AFVLtyx#B$>b*7!()Nx1y-&yhQL1;$(KrMsKnois9R_=D% z>Yspfo%E51ajqNUbhZm|I#jOqJAekz_Xu96RJe*&Uq1kq(23x|?~*VRKgL){o+3?_ zH~Jw&yMf9hscgSad3yIQePz3szFIrxHlNdA=M`}9%L=%C1?=FC`M>@*uq<)2$`D@? zB&z@+iCSwa*mcw@H5k8zM*i_XvZLiHk(WZ*!Rw_Rxwdd}wa4~r%z5~jhng{6#(2QA zp8IBRID%{mwe}t;iw&2j7C1_jEX|Oq=ijle*X6B5!mteFpF%GhRPM8$8g8jKd&42D z#Ix}88>$y;5!Bk-2@B&(fdL&Yo%GUYzs`%{A`#EC12{`t*cv$Ed_?&`gLhQNi#z2R)^IJ5)p?(rGi)8o@(MMhL~ z0X^8)qgL8=6lnwKX!cQN$H>YE9qf(=s*CEUn&|0vz9@aDyo5J4;IuS_6}yu0R~b$H za8^0>Ax()xuCFqY1SQSuwPXMI=s?|G>{nVy#?4;>-|9p_uhl5Gu}Z+U$J$2CYWDk| zeo`Odr=R|g0?VD|kTv=vuUSD>Q3a)aXT0n?p-(?fSQifyKzmPp_5OH3xhlIt8=Ic) z0ByX9OspL?B?Mo6xQyW2DE8$Mkcmr(!Vl|@4$hy740Ju7Z3EAK2<&ba5$dH<2+_6M z8LF?^7`WqR#m9XV4C9A=XS#XwLmSU#NwT>+v?SVAp*K5yX=5hU&FkuhoI5sM7o>ITl>@V3Ol2kVxnvr4dqgA`GQXZijTsl|>p$9_QIh zm))n=YNo?szdH^bht!*$wVKWl^qS7FU{0gW2No&K$SSD+v{pC_tLkN!7T3+4XBDmI zVVoK(y?j1bv46ueamC4~?e>Oh%kK8Z&0e>PjZ}Riu;3HJWol(nRC1(hkjXMXYTlq$ z{)_CWEpSmwdkE+>i#>2GfMGzE>^8#FO@35|+Ths0gw=tuQmG@8Ue_Jf^Jm<`_nJ3U z!SITKhV3mSC`7A~DzViZhX1@<{1Az))u% z43+pI&hGg3t9822jZK)_NjF|GlhsvipH4m?o|hpcR02@1qiK>0T=ff{vRE>KwSHH5 zve*3N_&^z5ak>HscIp>;uVzxVFI`}l6;{_Y{JZK8K+qMK1fW9|gW(<@NOuEtK0OY! zp`c!xZnxZq;F5$|da%H3%MoxEAgsuy>o|ynW7_oj1R88-vh8!nPqLdHw^~o5(6^uJ z*m-jn8-LUIg4BH%;#z+PE6Q*6LMVGf_d_YJ06KkBPm&P)9v9;JZe36f-q;7A{nTAh z&0dIWy9YUt7h1X2w~q;=g!{f&0cOv2;mfSh9?wSwCgih3%y|+-*5;srAOJ?+SK~g- zd#ESl=|!kCuyA%=o|aF>7uFqK=Qv0i}2^?4cmom zui#t&p>@%mANHz*_E5I9e``{^h25+mHOt}|zAlT0qS*h_|Hcl%2{$vbY|F(QrrOU2 z{^|a}3SjGsH2vwdA*0>wI-WUzJ4f2JZd*g7soNeyECf@l?3GM{l>MIYEW>#IcTEhb zsRfh!*+pxdS&6sV3uw-8r0Ou>d8Mw88G-hu_s%p=0s z5dIk0gnYjaGX%H9^g`3uI4`QBKI>J(QqSj`L(k?65J}cr@|e#d&k}R@5<}Wzd$_H{ zx5}F)Hrg#lX$f#LE(Ft!Ps#*PYNhbB`dFSjR_3gy5!=-euKY*{us%K*6GfcT9 z&)6GZE|)TXTcF%F&fO@LjHef-JCHj_cM=E*{Y6D2%sBIwJe$Dv+B>$%D?>W^2jr4d3QD`uD`}GtsQs-p9scsCOVZnF%Gj}lZ9EVo^T^D@M1^oI3{eJtcRVMZ~cGjzj zjkSuQ$QBG(TJ`Lt*GrzXBRSVk27I;;>e^PpH=WwfX*pF2A|}6HtGm-S;MKr=jlKKb zNb6w>LlgMfV959~_C*piaVe7P3~?OQMzi3nm59O0F=eD#Dxw*ClV&{0)~jaGYHu>Y z;!K+?U97V}Ud1^5Q>ALW)uatS&$E^BC1R0m*|(o>OMp@4^_pE*ZB@Ny*DLH*cUljt z+`JXr3cQe7rLU4O%Pjj|umgA}HIUeQdd-6m@AL4}nS`52C}GV0{ppV%k!29xu67AafDv}TE*S#U$aqcsjP*xrH%IhpHjuLR!HERbu9j{0G5*T}sXkMVjp z=mw^LW>|u<$9Q@j`-?=za-FgnPZ#K-(HzMoZNBtXY)@Vy$e)4^j(EMCiiF9TdKinq zFQ=y+*qj$atW+NeU3n^IlD>g5Ea(B*LM(G?pGf#h@0IIpwa%O}?3v|bf*9aj>3z{P z?kg*ifK*~%$8kp^4g?Iz5kV|;IT zrtY?<5X?PnEqjKf%Dl!SHHXf8_%qE3o-l=yXY16Lfk-E;?sYoNZl}}gcD#CiO(L@7 znrE3{lpvOAE(xXA>AxP(eSjJd<9RDvh}H~R3u8Z82kIDwDl9?4 z)AXND^*9owI%$#|iu)nU>#3KJ#;V3^ehLgnO}N606sbI0n|j96n;&QPq)$r+5^`}0IoTd`; zgH>ZD^`-%Hws6Rzz5p}4tBahJP`%eA)Lvd(ME>~P#Y!&@66)9zss?80?JS|*mnflb zVc408Xjmp>%!=5(N?zTeEw5^K+{>2NKmAwW7W1W2Dl%dF;!MttE>_wbcKdI&l$ZQM z5JI6cHfP~y5$K8g^v*KT8wVXPa2)cQ?H(Obdz?!N-;4DWlgUZDQL4iC>nZHhlx6<4 zc4lmI8|EHGeS=!w&Zz^7^|Y0(4a&x?S&GH1OC6G{%vrSa?{CUk zG)>w!Y)xU@+T51J(7d_oyinr=T>BS-2)vYP6#LhClA3O@-f`S!$LTg*$7^EtAtS#B z+&y9Sj?-_tPN(TP{kmx!vN4uCUi0V#^T2cUHO%w~N(){+Gy70ZER0QaG6^tQ2$5eQ zc#AoLgc(G)sCUW7+_$F1)M7bCwchHPm9Jzb-$tHnp1-8V>bBe^y1&Jh1&k#ZVX8;q zX69^hO4sd_DS_g+xR=VrH1A^y zRw!d@Z0Ks`ym`3aDk8gMCU9v0@7-aN-JTg-o@F;DVBaNEcxyMeOf)FY(F^ngN0e^h z*1=R-@U5g+)xhC*UzuC2G`4lHVln~4_^tI}*TG7E)Y6VoZWRXIRB3iOtDI>*Ord3~ z;e4p*`>mhRQDZjsg*ZIZ_w~WtIi1hF%%J*e!%+sBkvP_>*+xF!|eIx?& zv(A)q$bVTEf~mLr!f@%`Z*3S@!%bd@7)L7R%Og!-EPTjYF+=;(GpXO|jtTqx`E%aN zkI9_JVT$$!^5NxbKd~@p(n@QV$-Fr1Q5CtiA?fj~>lNPY4ZYzrr1$yU?av0>amaPKb9&zZigMMD>y;{AA3D|0-=q_lZxGrH zN`feyQ*^7_I3SDl)X{#eJS$f_Bfd(3Zx6YNCZ;GvwYykP3$BA4>tjNHZUip%VGF55 zAF2eKdJ6CmYpLqK)}^WteaUMzQO&*LsmZ{Vv0+mrzWj3tF}Qh1rkcy_r3%p<>V->5 z4O6}-B}&b=5=$elQEK(-Kga4doQ-;X=s2{D?YepmyKb|~8tP}Tjg<2i9!o98JlX(7 z;UZc(&rj*u*n=eIC!qAaGxcU?D-{1ro-P147f9Sm%t+AaYktI22wZwyU6|&})iwKY zbnx!u={xw;C-3$Te|YCC1JId?rz{ugt6G8jwJm@qn=3kq)rES$Ln_E%iSX5r#@eF{ z85A&F{v3+dc*x!9&~+SIvvyvpJm3#jKVz)6-GqSN4eR-dsFYjxqf*|+q1<>>M&`|x zzM-D5!&={X{I$Mu<63X6xbEv(aXW@$SSjhgjCF2Z4n{~NSQMiNc&=J5t&PQWXJLJ* zvO=gS;J@4tf)rNsWO5l)`zNPvh&t9{1K#zcBW^bLGgs&9d-wfLo4a-xiw=* zA%@@H7Chz(8;#un!>k>^rj*wt+83*Br`I}Ajdu`kMA1mpP!kl(#!S*=9f=%Frckk4 z=n6If_o&rl!j5^iI0paUoAb80=lm`Ehkq!Z^rgrD>=AtG{K3(mKVi<=P(G z8d`|Q*APG?Y!tQ1!;4_1fXIN_p_F}6LgwDA?tTC=H-4=WGWTwv+SYzh0X+S3SGXKn zA`we@DVPN0U#)?#E16u(B6&qB8TV~HAFW6u3SPN6foA*SE7I507sTRe9bE`?lY6V^{nR3I!|ub;Az zu}sQ&pcLSI^P`R@SqHa2U=JHpr_zisSDOvuDEdCjR^G> z&dyyh2X7fR6&F?voK?Y8&Oho#Z|t#l%aaSM??$QA%UylDhQbq}xv1Jn>)7mACRtkI zo%((~g(X$j`GCweDxqGu%qHnd2!HX@R2fR@eNV|#NnCWur$e<5UpZf~Fh&5WH+zF| z-rEC^?eFaR7Md-vN@kkZ(e(krrQJnaBK#zK|UC@lqv*>xy+!3Id~ zIh38j*d2FfuH%r_vpc|dr|G(^0lyt`avz3ivW5-syLB)ZnkaS|#{0HHUUAUt|0Q4o zeM&_EE8Jm6^H>t0s{uqOzu{jo&*`$Z&7t#G%yrz60~wE2Qo_yOqgH|^2Sq^bo!W)5 zj>D{#sV!M)aMvPhvK2uES1`RMJoXnNX#>Nnx>eoTUah03?Ru*5P3)RbgH{qLmdQ^r zm+Gy&e5FQHy%(Ok**-^A@$wfsRXeS&TiXPw)Pu_MM`sdc0F!f|m$gw>#W#Q5dUVuZI-IlygN- zX)$-`0VSH#X}dshEvRg?1UB4auH!jfr-$qBkFj8K#!ka{9*NV8`xi}i3~7vqYaY=7 znXnU7fz3f(0{QU!p*P{`Jzs{=wXR9A+QI&P2-8eG;OLmm)Rm#}As-!I_85%s4_&^~D|NVLj7Xzw6V;ibL5lP?< zs0Iyo7gU2{vkyWwcqlwUMJsoPC$L^_2Y7-;WtMV2L}4sbZ9fiFqCL#kaV#QK0LJ5v ztMNuZoHB!x$0LUw5u(33x?E>#9!1woeDDn7z;8CEda?GYPD zy>0PY z5+SE%iCj9hf{wCbV5#i|_Ijh-hC0-J{zLd#1eyWv!*}#K&$XQe(BV!6t0jo2Y0}j- zp81u`M}&+qQh#=EjC&yc{ps)+G?Enhl*9PpMpjS`%b{)?z?`pEQHb~rQ>i98J?7Kl zv8mCqQO+Z|4$ROdfXL7%UG=TK!))>Oyv!YDpmvxOG2?&_q@LdeF)l?i7a&xaKT4`# zN+5|$5Oa15Adq$n%H0X>Shc=;I_ny(PIbw~`5W`!$ak{k-nWRV+;qo}Wz?0KQj4^b-=Ccwx4Q;wmQ0E?_rVt=g2P6y8ITScrRL_3Qd1p}Nfyft z4Mh&)%Hl+Uu?A47pbrE2lm*<`&7oExU1%%0=qA|by|_iINmWrMYQHQ!t#BQZHQ=%c zXwtOxqKv8rQWaR}ox2Oo+N8Pa10;f7#PTX;fF()`Cd5#(1gI%83D}MTbQvRSa3SVx z9UQY?!v}ivy?V%7hnf#l#v!kTCvqJJnBg3zl%j_li?XN+HcsIME!Sc`=Iwt1PF~vX z4ZYrY5B~7wQc11V9}UNY&amfVqcq_N9duLAVB0u+OP_fAP}=3=6nSb{sgp(g+Kl`u zkm#(wh@3I5`h@XT7Km9GhX!)I-$Bl(P9Oa|4^a%htR1)_|Iuv9L#N~PU(cpYp2~mx zkL0P$Wu8;uzZo9$@B5rF`5 zVO7S}U$C%{U*=N`!26NGC(+}Glsobapt7Jd*!Kn__c=nweB7D(1IHmVVpr>7&;^Dn z{Q34<#%HicU)zAttc7oB@`!TeXPq*ybC>KBtfEd5;h(rnSMcDlb+K89{4)o}iTnOw<8&aZzft|zS6y5{?=)xbt6 z&gvTl4FE_?exP2kt~h;=Eb-^ScnD_~)2!hNWHGzu-Bo^>6kD15byA2d^qD zjIsqPE}2~MBoM*(L}#spS6J#vR|QY|a|DOS(>|a1J;$MiYvl$x_2UjU9SOU} zPuYgmUEsW^G?9`#j;rt2htA|Aui7aZZ0syqr&<2R4BG^=h5&o>pSRxYD_UQ_&8cTf zAKKzI%!;hB=6Hp3*y@d8ixpVx$IO7iZW-Q8UAbi89qBd-1MLAlD)QK(-{&@tSoAK9 zU`%vJ6=OnYB)8a1N=MhP`f^!5J>|*f>0iFqZ9O6x{um1^bzZHaYxaXx!zJS)YjDLJ ztMW!zwOws_scdm^)xP7smDAK}FyDj+uPPYpj%{236VWe~di}*~Ss+S= zqTB6H`;J3f$IcZXU2u@ApWdKc$zbDalWS`eGaDzsO_4t9)gkF zuSlgqNgSI+Dvhm0Dviw|mB!t2{x>cw=(>#?i&PqSDN<={D^h6`%24Lw^Mw7EvuG{i zEcgk@$$mmYuAh)$?58&}3PhUyLoBXDn!WwWX_VS1ma;S|N?CwyNuNmW^EykB@t<-9 z_NSE$ep03ZyyrdN@{RNL+rKEB$#^ms8CBJ&X2JNl@7w7@9er-IP)DDsD%2^StE!{f zR+3Y$McFz|?ICr?aha?&^_KZIJc5Wf#rF^8Ra}sU_`FIcY`yx^ALKMgg#0u6<{PY9 zG#Ad)7wRty$D)l1rT*s=yvT+e^#{Kc37FhhJc)_2F8?|u@@o6W^>WFR>j|stWCis$ z_0m;=Nc|*S;Ww`T=}+u0u$RtC2JFv&R{OJcP>&vUCQMlX4c1i8eL#BGnWF6(f6wN+}f|* ze_Jia3zpNr{_{WncSt71SKJRX0Bs;06zC~uuotjIJ_mTiaKO1g?m7-FYCETD_K^q^ zegVC~q6jpL45W%dvfYV{1pAJsB4GQ-cYPq^ERm7H2#dlC0m2}(0vU-Zx-e6gF6252 zATJQ|%Yqt9rrfUxX18!5J(`vC)3B&7WtN8&p0nvOWob@LZnI2sKapvgt4yg=eilKw z>PMMpSdBZnlrGT#UqI4;Y>l9Q<_~acwuV-OcBSQNT^dNFCf<<#ZqSl7*DPOWix~d< z$F>q`d(UdLWi`s@^F+*fCe9>ea`Rvr#+1WtTq#m#0CIMmBg0|JF35tXX*iErm}xF# zGMgEpo`N&c{u(l=_(nAwca9Q*?Pb|}MFC1getaK(&QmIKd_uE|7{R=v{c2cozV@jw4_WXGGXQhi-p0?dzx^EB ziM4f9fLOltHDE4z^iwRd3471Oh|Lr3i`hC-9}aNqAz@WR-VuPkFOx+0S#%8(f0%3f zuPPGIBLEJbTtp(B6axY}wc&J?2p$~BIL#6c@P$fb&k;7B`JUe$x{gDO=FV&77{bQt z$L(+Axm2WSuuMiG(TSYD|GQdat5ERto<=&S7~%~=1n&f<$)^571nWps-()4!HPxt$ zp?4{K^nEqgD1q8BuM9W*QWP%v% zkam1518~tpyrrLGvMe|kpED?1lk;ni>Th-ZzQycq4#_Zfg;m@%u1txkXUG42LIG;T8k(Prp>AG#C#(o$B-OMn+yg2M)nh-_2LmePvWA&!+42 zdMZCpT&L?gZhqRA%O#J43HyCLUDTVb-tyId{l!uStaWn92AvLT2R!5Luj_WA(nhoW z{N$EKGr9H`)R-CP?AO0$l~2PH>XQ%p{jN8G|CaBrn{UoY3O^{jF5iDJ9F1a9x7x=U zTdIqXdA68f!^IW+6)7#SR(%MUVFn%!(B+hAZl*^fFx>)kVl;dJxH2p#0PQR5zQUD$ zTLF=p3^Jyy1xr4!ZFK!0FGT?5q!iRa;?W?y4512?B0VG_&*?bB*CQcJ+>gRlnuY$w zgzb~%tnEbYu+w($o4x+?7z@3w>vso^L(0O=E`s5x>5f^0e!u-TUs&?A7pO+gft38! zdcx}6PG?!yhm=wUIXP~>^Fvs-XmPZFRx)5b&cc@VLEOtB@IZIKQD(&=b1wKo>=TkW z)MFEIDPj$4BI8*Us?lHcnY1<>9y`2O>!FSgxbYF*4eE_nGT4u^P!I5lu0ql~&EYx; z<2m^$!gx+7n0WZzXW_3Q$to{i+Hk=X%9_9r^X~IXZC9AyRjhhqEyP}_uA(Ymtjs_` zd4oss=2;x>8_&S!+~W1IZ@R0aXG55cy@$xMv8@EHv8@E{w$iV=mZ0rcLT2B8Cz0L7 z2bt@DY7ZK`wmk?UemGFr1yMy{FbGUCL-GUp(NstNW^d5%zO{Dl*HeQ}QbQ~M9i-!h zX$~j$Yfz6#Oshnyen0)L-R*mWp*MCM+P-&o@%6msfHl;Q?cy_KDK-n8CKYL0gd#H8 z1Of%+53~1#$=Rg_35qL+HvAxYz%plVx>vTGIekrv-E``HB;p?a=G1mp)2%S<~NL~-B0!|4?8b}Liti(%%$^&H! zq+)p~l9n%*E1reZNT6qdWqfX}C$9%S9zxpP3X{iI2++L3l zx^uT!wI#$bHPLEiS=~jPug9g)85S$aIm@nB65>`ikpu(QX1Gy*CF_3J^J zFDG2yFrE`mMN3ndk)5x>9kktV!C9cu;IG!XDTGNu`7kv>wQ{6`@1j>ag!>eqloNednqjut(nTthr6 zSV%vQ-#qX8rE@s-tF?HPjDgtfZ7den7S4wL&40IjV+k`kO?qR};)l zfcTSAg5D|FnVPjsQVtAozpe!n>Ic$3c&bb)a*e;GyN_a)%B5gFPelr#=>YZu3oIn6 z3G%(>Mf*+gk;Q=s7#vq$B+t7^Au4q$1l#M6`*17J)&?TuVT8+xNiz{?mbT&J73(SG@y_5+MYyGVfE&>r4+1y3+r77f8&NauU;QmRf;#j>25VoW43V{26vh{9 zDzYmfV&o*pAz3Q0Q8N%3%dca;+&JC-)!*tT9c9FFi=ke-Tulu=U&farM!WEqcQeU4 zxm5P_u~wnFhXGq*PXC{0c(trDBlR!$+uT6&lVJOWUe$&6GboME#t&kQNF~s2u}qXE zTc?5%Gzo^AjY_KH&KOOIWq@R{NhXp>Ir-R1yUT%6?0;T`7h#*Ga7#7PQoA967!=mR zN@M9ouZ5?jbucI2w-jaAPHbJ;C=Sn|H_XIzIB*LFNXBZj=i29 zO6wG@j;4D$Zp7nj;HyALNTyaYtWW{=A(7=v=iRL}R%NZ(bGtI2@}lSVM5%>8W$UTf z5>esl1w#AN4#K$MdeNT>l!uxn#0qEJOB7;f-0F4+6D5&XKPhwJ(`ugZKrnS@lld9@ zHPdiB?9JYwJ9=vi>XqyyMo_j^{r%b5@hL^wVVN@V`io~UHD@B|@ex-VHS8U`XHax7 zgCggQwyUIeNO-sEw4DV0h3Jlda&IQw3v*@U?s zV8uJ`<^@CtFG%P^5K z0=w4hwHm$jellN!pA{x`i`82#JmJ8;7mKS)+%xKh&1m%MHlvx~8Ny#g;Y|3~euO&R zDiLWWSID3s(=5$+@(lh`cg)>x*LNJ+r*;n{Anrxe9o|~_x>VzL9Yv=w;-HSbe5}X~ zPgy3{Ct@C^S#tebagEn~H2A~E(iSZD4RftIXuAehi8#&V%JSxk&%(F#{2iAcyvb7T-D)$kcK!?8c?^@Zcm9Ncy4ja@bM z#xBwx7lBx5%f17BYSoo6;O6>@?Qjx%fCCZE04Bw%JDy{JugfQc2E~SH$!*rv7 zi2y^ccnm?uRAh{s{5We%P>;AJA`7S6S(LW-sDW0gfp-`-hn!qP;fravbcadTrN0mC zO)1X;Hk#u^wF8f-H43D_4JA2}k%T5hnDnR2=Qvqy?#_e=A3DxYQkK_O^&Zmk(!?)hi7blw$xd%u#$D585 z#tj<_MEe0_xqa!{(yG*?U!b%G6t2EyBz1DxROXZqcdeiqet8u%oq^GE&`uS(>zrNk zBqUx6K1T=eAqK^ANmZs2zf?(ircmA_&n4nARGt4mOf#8WBP1J0$~cw5TYkvJQpTs^ zISjV`aMb5h$03Dr=kz&H6J630OU`n1kL53m4)l-fVR1a z^{E(IV&8mYwFSv?i2(MglqzxTwq+Es1N6D1OPso0yN8DyxXB5N9p z@3+RQNFO0gH88&0d5e##`nXh+vd8uTj1b-}TnhFkjFq(};~;&@redb-Q0p0ho}Y>s zD+5lCe)HR-4&LPM!GmBCn1!mf%i**KNT2KKal4TgfX#GvXeQ~@|v}tfOmth<( z*Gm=~F;TC~BVr)}gYAH^oMauxYBW>7RTs^GZCfMRN0y%mi3d_722g`kXW$id+sZP| z7rHjMx9BR+@0ul=%4KuM&3E`kZoViQy}`iTe79U>*Tn6RYW8)tYUVTM;NmrCLG zT;Yu!ht|xUm&z{qw$u*-@95P1a>ybNM}RyI5}BqSbM{UX`=ImhD9=n?qQzE-du&Rz z(8ykqo=|&+T9|#yBYjm7PRv#YzF9%Rg!=GJWm(JMn*s<>&UaF-Hu5SHNMGn!?-*#& zeSaxzw<5T1qsua7%QL`6yMw9Q89ELP_MJf_aEz-T4+nV;5&*-zB9(rRrRyo;_SWlw zY8YK3m@nh%)QV>dttAkw0DwB9gC(fd8a8%2CC)>pi^x(D`^ohxcc|_mjO&6&k%;G- zB06RM%(PK9jGn`3*lc(Sl7bGNX)3Q zVJ}(?U?v>5cyQ#v|MkHvvOX?lYEimFmaVG3yG@l|zY^1WJ}Q?;o1{jA8t#Z$u4j&c z0?A|oNZlpiY;^M<&B~BSNaB>Nj7bc zA{J;Afd-s5m6B>b`y1TB_Nc@ib@ab>n~YaLo&s*WFPBTKd{wuAK1_5~Ax!+zp7C1r zqkhY)QXP*I`T3e$6GO{2;f|{UFau|>n`8#g;0u(D2KT@k49jB-9kyjs;Y1%eckl8R znRLH6nRHuSw_>yeVx^{5>|Ymcj)Bv)rBvZoA2{BwQu4k;DcKG`fIiKnwE)Xlq{tZ~ z0=-@uDpeLpAJRlgLCfV=;d+13@;eciBFU=F*@5HPqNz~vU+M(B#h1J>>ne7#7dmP% zokv^xg*+(K4NEq@rlrtY=F=NP9gk0kou1>6RkXWr)I;Z}cRz#wCWD^cY|uA;7K7es z>@8L&7iu9H(ydIAe--0=1MTTLM|(=C{97XgK7Fcuf9ATKuH(>1-+6$$SUc}_2w&?i zHP~fvtj4{;u^MG?tVT6B);5eOgrC|3nQBynOf{-NrW&uCGh>g<&HyBZRc7^7r=YekLwmnC2y#dWLqmGP1)j| zD*2#nu~AXBcvG?1wz9?D%BU{3dA9N{Kj;O$@zil>Q185%b)d9T{S1qHnW+lTq%WfhTL-He9v~rXfVECcYBlaV zWg*$c`s%x-fq4uM0+=eysvdT|eyuS2DpM(6Qe1P7)ij7HPH8kmz@cotrL;fN)@XO3CPzUa@YY%^h)WygY)&6#Mi|7`a?N~52w)3-DHvy;4v($sBMe;w z7b0KcOF4FhN4HqH`wZ;#_6|{*2K-W|xm!l`0tCp_d%g^#@Hyb3yWY$fgPG&dRNuM2 zX)u6EG#HR!Z3^0qTIxEU({*|^186H}>@`C# zMRk1)srD1Q9aLaG0F~~H9UZe736UY7(_&qW(QVoJNaKBduoC@MY|aRhG8t z&C(Xd`_&i^);wwfa7=qopT}nNk7~RGE$BnxuYp<(_t*jA4ty_@@Q+f>{bL#I6Kjk} z2s_(Rmlmc@WnTEppE?Vkh-0J;H0gXg_5n#1fd&zOhQzO_*YEWQjzbe`XVetfllq;? zJaG1Clkqwgr|&p4 z`ghNE95vl8YtZkv-xdj<*&|Ucvr4LEwiDIzO+?E#(k$l`%SB@3?Oe*2RXob~U{EfQ z0aJ*j704wIV`nNdUSb*kql|^kc69>_a>bh#IgD>1lx~!np$kITII9J*apErGu(`$V zz>a(`Ze&=8UdxuO*CE)FsZcAEWPY(D45B<=#ArAZJ?{CALyObyPPP#`*)$JI(aUyM zUs%HqFEt9`rAB3V=>hSb()m=x0gq*Ty_D+|stSt^;mWkn7d=F)U2@N`Pjbc8v0XW`ZnIV>q-jgYHF-U)$T5NtO}^$})^k1P^o~6_c@56yAQu zO8TJ~yQ9I>acC6pJb1w&=?*GAD0ZRy7g%84_(BTI8>~D&h>o-1kO^$`1yhb_@%11g zc}rV!Og+ly2@B&tAVtmFt)-0BkGX*n$hvBi56D^ohSeMUi(CMZjQxH1860#L7_dcn zyb_78RLJ>rL?1+F(C_<7A;w9+v$xa(zmfXsSAtAGhMRF47pI;coXY>H09Wg#p^=!^KH@AcWDZcd#;XvUJU8OnF%-kLBu-hE52L6F~I6sn;x7 zH>~rdKvt8!S(IcUta1i6){AFebchaF%*UG0@Tj8kkcyIzSVOG!^< z5-}B1-8e-QAO2Llzxe3e9}4^s!nY~)G-uKo!j6{KK%;eAn2ATn6IQ2CZ5_X5AE7Jz zz0Ge)k=>yOX|qnJ?E*22e(ce439nzrfE&BTfE%~PfOm+d4yrQMUgYBA5?Tyc{FQmp zeGBogGGn~IYuRD{ODj7BxgZ##ovo%_OZ&%1Ijgr45>2HO8#uc%gZxE`s?&12mU9H4 zz%26y84;-}hl~`1#4&x25ap~h5L18PIJCpPFnqRWGg}*X#zp(LZWvbP$-joldw0(! zJ2*LfMz>~n>QB4=9Go0J2ukc;9?zc-E37gl|78#K@beo# z3EtvhU#ayB^Au2pRD*m0ylJVGj9$y-^;#65ZSoSyfk?7XCr4J69a1EwX9Tjva><eE*n};$yJo%XUYK?s%)*7FPAr~OTUGKTaE3v6P600T@reA zYN_^GKo+~#8(LT&Rd(1bkeSUi{1yxeUMz+(ym+4pw}F-2K@KiZ%gS-m5EwmO0P!_E-QoUQwr{mnM~AP&GK4v=VGKjA4H0|O^3Zx3vB(;or;XRP{gtMB?xhG<1nk&A@6J7&%#R}zzZXp zu`tfWoM<&PqDNuztT6Fj?tq6Rt{>#3V8;eysjypSk{!#yUPLQwyO%2;uxa#YovxS4 zcER3S5~;B9dd_%PfsDU6c?@$KPi?dLh*1wkAU-p4Eb&wtF)vs=7JM6Gcc&l!53DJKS+-&)C^{H^TCS(V*$VZV}0O&IgS?wf%LrkO`p~Pgs!1Rq=uY z8D}EST4!LyTnMqM)fzS<^d>^7DNrrIlJ<2;>MUA+4yKz|Tji{s(KyE@wE^ZDMSwX1 zY1#rXr|V{Kco1~!Y06-suy+KhJl?oZV9T47+$oQD=ICyeIl3+WYmL2BGkU5j?RqlgSlNf^(+y8_ee9EEWDbF*)co&Y;Vs z;NjUq7)hDT>TChrlOQzv-eB_!+=TpvpR$1b1#CYbP8m5LpuSWWpz^dBxB?K0J8>~( z5u#?bbgbA?vO3jD#EaY$kh^;(c=l$FxtMP!N(qGlDN?rL316}{TZR6GRp<*<_7E4* zDV$HgSB$5#aYt~+p-p4wa&K6uM!n{EOS$)E<@No`sT)sMPF-4RXd(k-oqj|ZGagG= z=ZurPY224#hQLH@Dk6DhYRKa}LPdRsIY?>WJk}0$05Z*q(s;Uw40-ZX=C5*_r4lgWg-WF}EOHv@5Iad@yoG7A=Ztoh#^_6wM&Dv6Qqmx3 z9YAz3@~9jgFIXPsqAk7(#pg`KFizN|iyJ-3r<&2~MapMD{tzV}wMMs=BIC*s0V!#( za{WV?W_7Br5=vSewf>;;xxujJFKC(~|+&$#b6q^Il-{n>2}Sp$E*{gx$D z?o-+A(M;34BXuTeV;0O{%S5wIWQ5?h>L1^QaR6BoKwCrTxF!NtBy%NVOHfL9hS;_i z1*es>VqCAamVx5UWNuj;Lpj4b2{S4xgXi-!4d=14Vo2}Nv8jFEhw6*VkXxNrwu)60 zL~l}SJlzxLTOP?)<<_g*&_?eWZ{bgSk+ZMY^2Vh+3rxx7t5EOt&k%s%+zSR%$Duj7 za{vPHS_V{fyAgocLuAAdi+k2_n6)yIW-uzRMb>01f#XSkjCq(X)>Frq%XY$Je<6}K z1h~|#>dy9R9Yt-|yH9kZ@zl|c>M+KAV;Ez{9aYCLUgm7j5D^m=LOFlF+UAh3JedPP zOc18OHln0~AO^H_B z%Kamg7b4EN(9TdMIF1gDJSfaTf9CXP_am3Emi=LM-p>mjeuY{Bs3GV%qgpPH%6y;U zuJRauf%2ft6~VJNg(9A}6FoTr;#lcOq&13^O?fH;wJ#QKd4{0mXfWdAQQvWB%I;oK z(8r2`el@}F-4gw`l@>g8ofaKBI!0LVB6FQdrWSF4TqMOQUzwiuf`_=;anz*-%%Zi@ z_NR`js}X(tHwV@_P^k{TU?H6?5_!c$lE?(PbHrz5cZ3;>xH5lO#^;uJDs|wODV~l_ z@VgM#S279Gw-c6#rMwiN(JjL?;}^n`O{Eh2l4YZ|kcHq;wtxhM^jId@+listRI8J` z6iE_-!L1ZM&41i3@ib%f{poj;{(w)X$qPAON#gN@nqhLsd$GLEht!^$Po?C5yH+_C4lgOTHQI*uBX zv4d4(A{V^E-CZFOyI&e4BFdwKgCmvE zrC@vAPPa*0f?d0TRfv=)2<)G(C9R6Z=0!{ENU87OPP1zmo{{iv(BVVR4IGEI+ugGl zlw$98uuKl9nkF^2S1arI#ngn>T_E}m9}AlbCPIipSVU?Cxiu-Ww$004A3(m0oog7;iXtZeL_=6S~!@0F^FZqoHM3e$xMP74Hd8}5&MC9N!H zd+}qx?8E*+uHy{xI(71t*#@71yk{5$Zr^nrnhiVaE-pCJ)DKRBQf^f_i3-B0bv%P$ zuXyMOzN*z41?Aw<7FQ-Q{|pN-&o%_ts1~yx^Ly;QOt9WH)=M_#t8svOT7;{WNU&@i zQF7E#yi>;+G^S*JrtoqQFo@*5Z4$E7TYgX%m$ecLT}Dn*kzCR)p>9vxH3FZ?C(fuU z0Q#aQ4g#O9qbyuSf*qB)e~nx!9d0jBE*~ z89fhO10{n;hvABo3$qHq>_;0z**%9JPZ{RPJ8XZoimq8WW9JKGkiL@11#X1rcP5=K zTRj`1Zfqf)ZafK_w7mjU+8{4-qzfM>w=1^e!A{vw*F19f@~93knV~pZGFPJUa!~H;n!1vxpO=N zJvEo6Nv^*cWH6EIER5&v{6>Tu#M`q-qgGZ2nE_IQN(I#80i;dN7`M<412GF@*x&R$ zQnNGE>-vx{l8nMPiG4hfOCH7>mZUm3VI`PNRzU5v4pgCbmQHV=b}G~0?qH6Mhk`ja z_Sido>_q+BqOMSXGQH5yUyP-3c$y`lpJ|JmGKhdR9jO=(!qq|~r|S^n`uSBeDW^O! zE&1!@QXt#9h?TLfPbhI5z{$SnZXWE8dbLK0VneO0T2MN|zXZy9GMlJ0qp9qmM3hEq zsxF%$p~X^w@iI%~(#{zgzM?hKHP~Xj1aw3d$Y3+ZOe|N~^o3WnffPCeCuP+>jX2#BHD`d~R!=_smqPs&a z%A*FlHdToo0$q(_GHoh-p0!qq{CsUlUqKOPJX(dZXu|~)*XcNYeF0DAs$=z5T5qy? zYpwpf4pyy1_?Lhi4hQ1!b1M~bkjY7#dVxQ6^G3GEZ<1%U{U`R`ZMkhEYZv^l@f6uq z&u_^jz>_3P{L7A3$+E58wycokuI}yb5tBfYL%Z#l|UfRxz6ib)j(e>TGuubG>m|Z$kopT!=T1e z0B`Zw=089&rP_Gp=CXL?W;GtUdBb?*A1unaHkay$rjAE!9#NkW3xX6*o5FP%F`vp4 z$j6eJB+M_sRMqBL{iw?YF4hx7uw^=gKxLsnPPWws-3qoC^*qHl?i^se*?K{m@c&C- zTjcH6so%P!kK@9PH{?qBI<(L}=Ygj@Bv-uxciCD!Q*~`lg-~GO^T-2qe5oq>7)6z- z-+-J-QI|=Ln1%_F;;oW=XLODU}yHZwyKR+_W2VGqx0l&RtUfEvFU6 zR?8jJEvTs=@&G=1_T>wiCt;k9N#p+zu6mPQeN((tjkK^UEzFL=oYzW!2;LD` zH@7yW^M}5s=)QQxwoP=54L~T zq@4b5Eh)!3L{j|ONz!mE@9Dd21nxl*raW1Y9N0dLjH9qNzwt+1qOJzR9VPq>!V+nc zlF;Wd$&x5vl3JnR2VN4aznyYbt+^DbOn9uB&?7{TZ+2g7jK`j*h|-TB1O(=HnpzO9`04Pa#MR8{(F0GYd1l}1O|Vh%NpFdq}b<9U(_-8kS;$OEt3W3LiI^v{y;F&N>g>AGqw_o(Mhvxcr^v89Llpt!Vv&|!2m3wWJiN%Ot z4!T~yiz*=g?DN@-#=(GmZip%P*L?Z^{&dCz;vSuo%}xiyW82Rg|FHAcen-R$)3lU* z!35FBdyAH2Q^hvnmkas0uB`3D-{=>k7wc5sXmz#k?v@7IHvX#|*rt!r-*`rj-+lb_ z{xv!Hcw$!*&fNxAR}l&#b(&4E8`m3>a^my(0?N2&0^{|_8`$naZmw^uGY(Ew({Ns57%##}F-S0fvDRlv zuJ>8ySEn}l^+&xr-8{DXgA^in!4yaH5@_UHL5^*C-F}@j;1O2l<|uE)mFbz4ff`%P zT9n_yvX8RKfb340e+JhPC_S34tQD`ZoIkDj?N_{O3O7c4LLzp~qM}35TJrBBk^Xo~ zzJ-O|;*=yT2ovUmM0UYON)WQExIH*-(2L%S&G8G*Q{4K_LhaTT2(|F9O16d+$;gqK zw5>?(fcZC!<&kQEpz70@%;*^^sx)TSY6uk*F;_o0;e&iP zw180mBO&=8YL;^lJGa)?`*snD!~Ch{s9Q@=`JV37x85lLtKgGK7*ARdtRTmcUoy=@ z9{f&?Uu$K_S`~{nq-{J>=*^PJ8oqIqgl%X~)WeN;;H}WYa!NV#3Z@ zvPh>G_eE1OrSmz9L88>}hpAxEn5>GFCTTWr71>NnHW8uzjQ#rO+h;a3cq)B^zq0eI}y}&k@rWRR(ur`qh9a*{ON6r z9PS+K@3zRBos*sSE&Q}ae%U!Xz;)R_c=J)!e1UHV_zHwM!<{$Fbi$QZQpQH8*H((t zfHjq)x=6w_HLweEcDt~w@;E77g{ zw@G-;^6d~Lixs>c3t9^B4X-hDt;2L6u!_;ZyK1w{@XXN(V?tv>#Ul2n36Hr{#i*FV z;na?2N=Dgu4F1pK@Sm(aY>3HJnMDbkqYFu8^BKT85VW8MkG>k<1U+MHF2^uqGoCES zh>f{gcLg zLNxYSUOT8Tw}B*E`jw7b@fkzF%Hrgx3WsI%ipT_Ns9iZ;(^FR`|Mz7Op^*bH6 z-|02-cM_H+3!0{EHcy4EzAOJ)plFF4`ApWXU`SzONcvV897!)7xMj?ySPeKwB$XZCa zX-f@hS-1A9x9wJT=r84!)zP`@MXS zKc1+&UVxH;$OV;`@(7h}I-~yf`nKmO8TZa!O$dHcfgQ_2H%K{j%gD3V zdHi0EfFJKXn!wH=7CJ%dv3v|$ogko;dY(F%cRr60<7BLTcU+3h7(1inf+uTs~clZ@3s;QyZF#bbl`8cFUG+?5htRiPGPsqwSvOsmpSA z(7+yo2KKZk&XTI;xzp8>vb0#BrL3yBfJI8-sTN}Nhx>;{INKjXI2+_^SdKK%pNt!f zla2@fYmtj-=udTfGe%=U!t|Md3^W!?b%w`;MiDx_A%SE}eWucg_4EBh;ejK5Mi<~f zH`*9S)n1B>xN}Nej2R2;ObP5yc4lI6O=r3>tUKEzSJs_X>-JDZTyrw8{p4V|Cn~T6 zlUa%~4-Wv=V{B`??|F&_{Kn#NH>@~ZSdSZvZ`<|Gs5@5F>s?z>?*}`cFI2j_E*+W{ zV~!soo#MrKw7I$EdFrJ8#tvw=a6mV1bwF#Ss5|Bd_O8tj{NWC02Ru6%I$8)YRm^vu z@HtDYkW$D8LLvPTTs=M3r!QW3o;t|)R!jo348A{9&HY9eH)|t{jq&_JB8zVv5qz`A z;wyrJ4LVGHP_X&=mO;VIdd;)BMDuK3sd;|8Vpt2=`b5#bfud=HN#)Lxad`O%Hj|BD z+}rj&PZ6&>(<}^Q-nc$9;gL$m-k$`Ro<6z74)IGcN^;!p!VEwmTU+UI3Zt1O30I1(~t6tO_EuL=G?QYekDp#N~ zazL7K%o0CI0r#@Ry-jP0?K}VA1gf(!-ez>g4G-vsf+4NEL2-`ejH25V6Q}!`N zsYYOoZg1;D=%^4{C|IBnSQ-tBmek^mJ{|FIIN~U^zdqEb`Vqo6=r+2@)WN=cW%o|2 z+a*o-_eME><9_q*EzAD0B0ZQ-T`tmd!A8XxQ!AjviZL}$^{{pBcWE$NcNofdiWJ63 z8;oD>zChzoF?_icYkQM*uxcHB+d`BJC*5zcnw((O!ahC`tZyJ#9WDGEzWy1NP8*3# zp5#YBg4pWzM%!DSr)b&TlT0zkw8rCbJJVs67rS-JN%JI@|C)PaXby@Um@!mknbszk{1i%Bl62t66)wYBB$oU9QqC zdH_Jct$Og!r#toycWi69*$OcHX&h*D8)e6Q1UY$gG@{)t&r>(=?s^~IxXq1L50l3V zTHFeel1m(>3n>FeY@8A*bP%%VkV5c&j*H)H|4#yUXJ=;Su4ZUm>?E z3eOlxc%-6{<$sOPDh@$4Srm|wDWm8#(dyo;nzNckLp%({vcy~9{LoJote=QHiQBez zQ9t3rGUa2e&2Pf!->ftGH}AzzpIiOQhWh4h4fV|jGSoM9NTWtpt>QZCVh#0df`@@G z`@_9uT{D?27C+##p4OMq9O=SJ05)XGydmqXZI7QVxSs9^e5P z0(P$=nZ$JkOJ6o>sw2Jp;<{IygO=+?eBuE##7jVw_}Umc%|{%8r~d6nREU>qsa&9{ zF^%fSS6c>(eEx^yuBe3XD>`g|9re1mt{xV`ra&8|;Q?MA*^AG^gvWAVD%Qdy1O#?!jr-@3mEFLl9ifHRrXgw6m#MHBYN z2xOrn*6a7zJx_6?dtgFPIJu+HBA(dL{jeeNJ^9crISsiHr*SPVA&P9G`49|*u=pH? zXXR;o>rWKz?i6jk80%=IXr5yN*uxtNU-%_exFU$>@Teanm$#^*E&12T^2M0jL%rMuaE1b1AAy`Yqh zJx|fFdkAsef)Ll*2-n|K8Er^fE!-*yHI@G-+5Srf8QTsPF`b1zM58vaICjHz(@1!Q za5nkJn=q2!u_P63cp*uz_~MK$${(DuMZ>vMIdA)()|#82?&-JPQ(ws$^MwD({FKb6 zVZ;TWPqF^lRoq0an&VwD{EjK?Xvd<2JT zu(3hcJJjl)W-jWK^_0(f#3zeCv&DdnX&7Y*a|1r3VeE}q zN-dc{ZpS})%p8bIJ66R&vAL8-EYbd{&;LO0-ysh$VjrUP^eTJsllp^%ae#CSNN+fs z^CYFQ54Dk?C^|`4gy_vFCmg)FvR%(Gsn5CN7t6XorpYTX-XcIt*lPoU1 z2o6ZBZVJ?%bUVFu48H`2C4xC-u)-G&C`HAwh; zKtAuhe-EGo38TTsI9eQW4g_B;M9OA)K1GtnJK}A^v-yB*Zfx{>=HUU435vNqo8 zZ1tTpqZE^@h5ji=h!T~kF8Z-w)<}u)n2&xjurZgF%01G=VJx(9WzPQo}~~G0{aoj z+aH=(0~0hQFSTUe5L8mQT~EfEd%0Z*s3n=W$`F91hr0Gk6A;-*%CeyQtiN~!-KV?J z>-V=uo~JI?-9r%(0CJSQzC>l^&2Ul32ZIWefu!o+nU{f@HDqr!2_eoM4zx zkTDC(Gm1ih|7nr1&^MS|3ODS@oNzxk4EWbh&by={U8q10@rXJC^qDAxk3NDyKj>|* zk6w75x_5WRBnB4N>tYT_CDgU1@?izG%Zi?@kN7Ie)H+rIf+W_k#hUwiRX1uQ@*YeP zh+;7BXi?lcm3)GKC^(Rt~2X4Cq9Z0Ah^GIraV3Qv)k&<~L$LX_2Kn7r&UAjFEJWmm|yC)xlce1{Ud4-Ox zkONaS2RN@{{+7ED%2mvx(5FI@0awu^MUwItm(aamDVwmw<#U$MlqVXYD9@2tt6&}7 zN1NXdxd{z1h}TlH30Eaj)bJYDFoAnwHP>*J`(j=do}$^ui)F#fPsHonh?kVj>|KM% z`p1;Ux*I{)?`?RVB4u~CVuJ3oT?5B<%gfVzkjr!JQNL1rda*1*_38ZI`uz9n3<1E! zRpsax-_3RM`0LSk``xUf)}H9I|3E zCg1TS{3nl78XfXrN8y!7F$z-4Jo&11GD4m-kll`stgaj~`BVKOVf7jBQ0uxVIxlSB zh2;`ao(;Lm<^GTNc8~T>mUU@1tFFs}3uLJa<_gz{CTN?>uoQk^f+o2t%?+aRhAvO^ zd<=wfI%4ZR&r`JS?hc18Ivl!~A9ydB*!PkY+q^m{w&{?B3E=*gmY=ZykuhQ1d4%}$ zY(N^_PG{Dzj`0$>f*<%8Pv`XE4`*+@5p?@K&r_%L?m{5q1avoAyEdPP2eSSA(_zp>tt;WZB2?8NIZgb<{&Z>lTVGyQ*~jkvhGaB z!qS~{me2|Nl!lS12)x<(#`du)_D#D$=8rI{Ijv9;m52ICs=bH+x+fgCnq#aDGHADp zx^}>Q(ar=*T!?^cn|4!Ig)z2^ai8;Z{}F;db8_*$6!-5!E^P2@m8(NmSGG4F!JE!T zL2n!|&r^itH=`PlZ#6y>YoQ+gUpYJC4eCrC>+=I`z`5->u z@8qAg?m=z9*bjOS5LbDZCkgXY9X?3FuSL$-0_com)}@xm{K)G9_uEvsRwS8^2~85Z z5M-S2nZb>d1jJ7>NQ0HXK0=k@?art-+Vng{Iqq!H?zXyp(!_uL=}&jHUy}#3Uw`=u zxfPZ7lZl46@$PT>T_BYa@-vn39ePDk6G**>%V!vSe1=T<|%;N$!78~oyJG^EtA+}VbaiudV^D^Td)kEDWe?Yj}9 zaW;R1JP^u8Y|K1Q5tn;_jP7m#W;9|A+%i_fadRRxkS7lE>krt4*?1ifC0HEn#5`Wi zcqTpxC=HKB(=lW0CTngsbxp-J44HOs@b z*_Vt1{$J;3X=!^2zo}tVWjCthmWxL{WEK_juJD|}lX=2NOti=-OQ9fyypSp{Gfag# zX>!XJJWEIvo`vxQR$_ZyuEe$~GjlJs9^AT`4b8_W-kO1b# z06G2ypg#uWmz|>n?e70QS^PKZ3s?Q9;)ltNAIsh}dxn#0T&xc-mh`tqquyrU^OSsY z=k!!4#q*-og*&h9YFC)aq%dHcP%41m-(*60#0mm82K8?iMYLu?M5`~{XUk}0nT$rR zkkNn)L(QG-?IIa|5Zag%!n$ol;i+6Js3@A$?vWqyEJukZ25cQEP;K@>PE=}3h5gV( z1-T}$Se*=tV%sD568%wctKauLMOW^u<@B&%RIjh{-OH)D*CgS}3wjS3w9Wg>plz;t{wW5FrUw6EQVU}ae(L;%HR_M353S33KnYC1Pgsb5D)A{zuS@; zXJ1qInnv_jefUZ}!cYg<)<)+;*l>zCDa{qF>1hDo>ROC>;w$_Twe(U#;bfkrw$doD zF&={~C=~6AHK>XGCZS2*R^Z`>`>9ok0_5Y1DT|BoR&ooWYjqgZmdE2L^id~MoFWZ( z^fdEe5U2qWyv~v^o*ZNEVLaKN#N0gIyJUU_kstaq7qdqV984ZGB30|upYC1GC1Zh8 zZ~y3Gydj;>?WcrjPr-of$HiZi!c~0ek}qFOF$U7m3ZA?k4dRinU#%#XhrznZsd;TE zodmk@?+Vv%BqiimKn0m~R3t-|VZHM@x0Ro31I(9d=nLv1k`;{G|Bd zp;hlt|CXzw_=Q&1UbC@Oi6sp{@}Ub10;$5{kDS@I{Lq913r2&`ftti)fFJd{8|zz=f&eMGD+O7HZ0mKEZC$k6)`RO745r^~ z2K{!(+q)SgiPv~+fZK;PkQOfB?Kp^VM2>Y`qW z-`gPpH2|YNFxG>d)8yefxbHK%wZ7qbik07)gRi%`J<^o_+N$vIJMxz1y~f2fOK~x^ zF(%Dx17Vui1j01$oY(o~t1918PU~DD%d@xqKJ!H6B6o-x`e=>v*0b zAa|u3+n`l$t9;4FjK;b$+A7D5tGIldOI*GLUB0B^^34zS?r0ij6=+-cn7v1 zK+fd3dn)%uJwjEs_4VG^r=F+I^qr4#4^HFrFIVG zwBLRF^!_zD_;@0@oOXZnn9Pl0fDPqdULhY8yr{~f;V!>b=31@S*KhivuhmW|@nXyq za`=~BvbwUp^$3}TTch51Js5eOxhw3Vv-h zQF8MBm=vn*IL*R6ufYW%gu-3PvxaoQUZ>UT*Okt? zNld&XBnrai%9!}(!^Fflze{qyG&%6m0$UEXWy7t6|fHEm;6v%b7nb6I(>W}ZHP zcf)Q$Eot4?D*K>WmwnKDV7He|)P8RoDO9a0M11NSY31Xtu59%mgJLl{da*w8JVmlB4U3qMBJX+-2z9ZB>!THYwb^I0(7Bkmiv2!to?Y)4FJUB@Nz&>DTsUWFGkY= z8D+D%5k(L_sq6K@U=GSzA@59*GK_K{r6>omOH~u{KtEO4xQ*X!KLW<3Y`KC;_~J3jzpt-v_WB#!o~ML{d#lOTtuGTLtL5$?4>Tm zrFlh&OXUMA;^g`e7dztQfohcYm&NlvUEf<@Uy~{GDGpO|BDDjv`dTI1>=>17N%-K? z!hhSsf1qc~B)H_{Q_|AamCeVf2s`SI*?Nz9o}!EQkZ92DK&Amg#Mo&p^IK=xH19Xd zMm1MRq^`lhxIV$=rl~Z|M}*hNspArx^V220^(6&^_VGoS`ctwC)G!L;6udQ@_>wkm zY~SpXUTH-zMuegbpB-aqZFSAsH0jxR9Uj_ZO)W?}%WGHvePb1mSeL!5H>ft&p=U7uBSqmbhVsn^ZGSOAM*RkFIV27|(t39nE&ED9l=z zl6yfZsY37;nw6srnIJH1jrBkw=t{XB)hMBDkL*Hwvi+2Xv4G~};9EqfYIy+g)CoGmfN`fwlk`zgl03p{* zit5u)q&(5VAD<;NbCD%Pe3HvLlQ5lTBhTlv_Dr@aSllkYyXO4I(?9>V|7bmG@2@)_ z4&Uz`d$Zu)`{;B)r-OfYI-TObyPKPvoz4H~ZuB-c*SqWe&E9`>y1n(y?thTZy^o~k zpG>4Q`H#+>`zlZF-{hYaqIVvYw|ynfQb8zrP1%gc$1HUmtvqOP7sN$LI4BvuHk5S; zGGUlL6J*YX2!UyElzB{JLr_XUVK3?oXJH%+>`qb4$*{JLNF_a+ETHhe7U7Ky>-#(j z!gvxboE37+m_atJiP24|I1}v|jp>BB0d&{KPSjsgly8G%5U`Ynk#O+mV>JXh*s3S; zidx21E#;60WKI)0V<}4%THxMg%3?VQ(q^gOo^p{Ql2j51$Jvj;;Au%GR7fd&!Lw&k z1goMeHn7Vyq54ZLfeiCThY&qzWH3D@C5}kd!ugw$>y0Ht%D0+TaFOmrq3k~A6HC`w zVRE(oOr(4k{*$>d23HQj9rMJM>S23ru_N3yuiKEKladT*tdXc- zKLAFJ&X_j;xTG=W+>5wRqlS4jqKST_w=3U>*9XUH2^{ByTdR?4Y4DB|?^uFjElB-V z@Mp^?Ntu#|hCwXwNDLLe+tS&aH*pRy9qda@N~N!Lz$CqNDFma5KV{ZOvFLi@-1n6H zr=gzgkK}8!g%`xa!|y$jjmP1ogGf{;dE^a(=^zexDx3l+l@u%+rPo*xi;h`uO?1MT zP!g~JqB(;c#jQmy!gPw_6AQ@E&pW%0b281#5LBp=p4Ela#N_*mFzNW)go!i>{WKTQ zv5H&XrK>SBAWz2?A;2h|&ub~LCERXtKuMLEgc!%qaFDJmpGur!aF2l+70q9`0M zOnct4xB^926^!k2gH^oBLRG<@V^tKaDfdSTwf^v! z=7j_mI2s&|fjQUMgjMU${6cHW0B%)JI-Zo+sBlCTRb2h^-s_z;b!_b4C2f{0hk-$b zknO2+dScpMBcmjYCjtyH{33RS$FkwzBUI!Vl2y68?+$m?TDgPenUGJ2Na;e5 zAQY%T$rp#9t?E4@D&XR?efbO^!;o<}jf!LD|$z3p~g(T(!;-{=(Y z{}KvNr2$ncP^AM`QG%tkfZ&NffwrLjG`u_yjkClYuaLLTyEK|lDTK8t30W<^W3MEV zwr^+z$!kgjn$jZqYuMP4e>dQf<1C8g3x7uhNhHuW(?rVV!hYb&L;eesR0qn?+P}o%OWMsY=MmL+E}M%q zVRS}52iyne0Sq&zTmkp(IuBa=M_f@U(6#A2Y_E}v>0;nGuh8^R(q%@X$+ zdoQ)e&^R`DlDeuh^2b&ESW4L74Rl>&=x)&YyaX-_$zrO>260`UrSmL(39c+RCVw>j zu92mIJ`iUjDyE1LjhMoK=|YZJ5JJVdIZI|VW^o!V zjK>3PxUy@=43Sq^UW}5JCDARQD&=$7RV{@Rjsm6er+@?!BffI})MCiSk!UpQz2_Q<=FlS3)SMG*r88ztjn58D5MP8Xjgsf#j5vZg&_gh0bHx=`m@ z`7A-vw1J!TL}5HXtQw6(fG+$m-=7=Own>%y(dpPRicDUdo5MV`}n!c)16HWPSVU?pTkMd*{AES-W?qK-Hz z3K%U2738x;pKrqo@?*VX{?mmOPTVAm*qWp0R)N_7O_sZed9LFulAFiRS&~R?9Q0IZ zgUB{O0R@xR!l4AM5Re&5Q-9imbXebK^R#e~u??O=?l)O&uNBUJI%PBYz#6}GtZz~G z;a1ca&)6MnJkNnQH|84`yd>J*SF@50jAeCnttD?dw)W6TDA?k+(b->sgM2FSoyN zT5O-aJh>2LB~a2CT0f$CI>eucSeyr_MWMCa(50c-myDz3_&nAyAnFodkYO>gRrNXN z!F$!M;fpLFED{Vu85EraBZuddA_sx+a2|p6#g~KhySJMpVpEU+I6WuFxQG@u{nO zcJO`@Ef&Z>pa@@<<>9FpYjHLHF|rgrKV@y0jciQOxhN%%t&q915qf4 zCF8V0I4jto<+BqwxrYhUPI>*$i27$N4$4;RB4DZYPd%9_bSJNAivhR7LDi?_Be)kqsV%KbX0e2^S3l+_-_oe&O#s9pFaA)6QeE^UE>#b95-w z=a_=0XXlq=t?gVH9OLuYXY*8;-rbxp>1XW~dLi668o8>)HQDo@O=I~@wuoVc3+XJA zn!FdToRj z)swXb-d>5=g!&5?k{L{v&>8{V2KImjWblylcf)zzw!f7^+z$Diun7jt?H_8XS&6>+ z(!k)o3Fi~!lJ;7$coN1;#Ytr;PeK}rwst9o@kGV2LG=b#)vXWbE16)Y^i)l0%|>9X zU5xy8EKi-#8%@b)g-5i1Xjz&bVLtJxBKm9BOagaDCq!LCd+tllyGpt%D;FrA;WFm4 zbd?Ebws;G_?9UJAj8%gTl+^ZfS>UM=b?UW{I?*PE)Wtk-t7fMuc}r7vK^M2*>nXo* zQ{KjS7rD(7Y)P!)wUAdnj5D4IaH$;=7W>HpykJYbb9Pb|ozD-A{)FGJkauB>8Dei? zbmaXDINU_v;j4~Lu38q)PVwZZIe2v^NK%omiSJhH=9G_6b$e=#kJbF-jLGJGmZUQ} zhoiEaV}X94^G0DZYJh+ojOAV?XUvUWsDw;cN7^a3B#QW|>IN%C4$1F>9sErVOh>w! z^Lm({*9eTrCS+GjB(`&KUfeQEv&PK1Z(VHJ8i*>_E_c3CbDFR?Jz`@s-txxPZwVWs6n+l!~w9ut+@(g)*^u&`nOLB1V(g zn}vSD1s|tCt61!2qD^H}ZF(l!h7cDRX1(}u6OMUHAw!O%BxdN@xN6a63mw4sRzAR$ zc?ZrGL8xV1W?(o6#k*xe9V_AUJxiYnvWFxVKnbO4$H#f5hLmSjpr{-cd3|tftwX7j zL0Q+b5zK9<;vtZt+MrgRV-aCcIR@~c)PmdunU|>>TWGn7@ zr{~dm=vm)-Jejo7`^%FBS+NWHVRC`K8TCzylujhyx=GW=_-?V?AQrniV#C%uP-Gnt z*#02a#8H91D~$IXgx+ECIm$42M|S@Er=z{o-S_)@2PdcduN|8%fBAx~o(8cvHQ^$s zR+Rl|7^vl}ZSQz*_h|1VZ}UyUX9HtxRVBg-A&<&DsODl@9O0_7#en!x$l^5rso3-J zr=O2^kM<8w_CFphosY;y!cW5aRZ}?GJJ`8mNm3TmI9>Mf(cbaLPe;3Zr*Drwembmg zLEp_&xSP;SF`PGy!*tiA7Zv#+VspE5mdrx*ZK*{dxE!-L*o(gk-Fr>)zE)y4^gF1wX%qSdurZRVJ-IT4oZLpUaJB7i%qN3L=}r zE3v$J)VE!lLlDr0gvEghfwbTbMp+o8ZWt463mmXT8=&AA+o_vT5FCg>0N&vXHX2$E z=axg`!1875r7<>dxh}*Wd+~lRb-Y?bTxR&SEwYBNU9ge!8!!ye`FwJ*#9@O|b73{y zxw5az^=^E_=OD2+wmO~8&AwK;jGHTI5dTp%MySs%9aB-7>dCG18-eGjKj?_oTZR_X_22UIG%!3Q3PfFKl;oj)IwRlEoKX%Uscbtn0Eps(c<@39`0mkbi^C}N#H0Y&tDL%mgmzkjmCKhG zA1NFZqdC|3J&!S&Q9dCTIlU;ZQOb!p3+HB>c!yVK__Jh!K2I1IAflXi(b=goXLkig47)Z)(#`p;!=<8jNE3cR@w+Mj(UbNL3iViiaPz`RU2 z;n05B$}RRx1_{{jZHvBI(ph-P0%Ki< zD>*a{gP~K5f@BMbr9kMYC|VGMWg=<0&+G*|?|QvW&)LB@!YE7^aK{5SXEE#zT<4l4 z_6m&pWcLtaGCsXFPC?`wdAMJm6a=USEFqs2%7w^SEl^dy#`b^0`=Y)6qPM=i0)Jpq zOzLiIz1ZIDyjbt9q02rWwda(<{v_KadH*@MhhQlgVqmXXVNVXO@6q6#C26Rz@P>|+ zd4*`5>6FHZkawMX2O6v=A#hkUZVQAWC>%+mF22jKM zi&^*b7#|*e{OjM$iIfNBbvOxA8j+7aqp{DT^Y#F0d>R*P63vh_NuMqVwDGJVPkDeYE#>|Kq{Vd$1qC z5Od-n@3{()V1fSr3Xvq>uHAt0`8RYI*NQad?{O|J%){2JbWr~__Aawpp(v!SLKPs4 zVHPQc+&6^;BNy_x@`7NF$RxO$-) zpw}Q+6%?TqB#cKqi-TfXYvukRG)9h_jnT)%&qLEuiz^4^>z$5yz%;|ZpbL#P>Ddcs z7549G`RHh8?y?5g)Z@G>qG-mx5Gqw72v6!=5(_*3^}|ED0g86?RCiyY?c_ zxZbuI53-1V6C(PxU%y%DAbN0EdaJGo-9Qh z%ab$yq6(&3?jU{Y2qrSC)Hr+*>SO1|Xd2FNF=h)jJ>j17$SJq4T`biuY-fvZL7ki$ zQzcp<59`)~&{;1t={2$Ogp1e~fRl`!+PU94sN^ z(A;uP0(Q#|T74H4c8{c(#xw>* z6`{Lf95yjK?F*Vclip=sNp^z43 zElS;Np>DIcoT%Arr=mD8kn{!wPKN{I7xDx1e)al7M{SY#?VGfqS5P)U(`h^9sv%b$ zTCzg^_y7I>{D1%N|3zNsE9&BT0hv*^03rmVdHJ5>e8tK)O&-+Z*(@%ml9{DW^38{l z&gTiI{uD*^oOf1FWXrJe79P%sU52P^5Aa~3Jg^@}Va(Jt5P;<1W_b#4h)eo8T2p_b`M3;`oNR zJ-0cKQzm5_oL zG-~Us2`62z*Edg04)J0Z726L&F^}j1*XEsUlKdjuB!!XY)5!B3?EbTn(~X=&s>ophu(lG-hc#xAx;dYffp|6-QCD1ZrfPf5utt ztGpC^Yjur0SMPr*^p&tQOJee$_@gQhfd720^ayK$CjomcH3p1F4%~YOC|)hVFNsXQnNQ)1>}CL+eQl9AjIJE) z>Xbaszg+|W277mMa=4mY;8>5;DFb0HX-SEVJXgPf>{c1lg$M7he*VX3k+Rj&ky~3U zygl{gNIkn>=wq!gY+zHb$n)nr!8wgnI$`4ZbBHOCv~)G*n5C7AWlAhvYk^IRjw065 zLPMT^qKH2f0zhKj%DGtvz*5~0KZDT#FQpU;J)6tClf42%HH@Yr3|fK@oHh%Z?VN{! zO0F}?X~L+`upY+SV?irl@C2$t&$B2pGB3!d37c~P{s$%`vyw`lQ7z@Q5>UzmSy74q z@I*6t7^o5tZYLtiC%2N2S$k+xCgq7Gymb(RXXc14TzfI52+@)ujBu z^F`><3t@7T@daF^oit6tQI;}ce@w_I^UqjXQuSSYs^I%$lCUvL&}@;)<+g9r4X&D* z%MFD-(Z8FpSNb0BmO1X)OaseOw{>!HK4cRZ+#2XBsBgr&Z>rrJW* zBU}1~CuhpFXkW;@h>~7~)#H9kdDVZ8_^3tR?jD=wd98>>79`+0ZA!L{4{r&7om*PNiNI3$j{6jMfUVM%w25E*lxk3MEnK zvsf^)ie8r)57?UH*zfu@CP^08e3SAs7JFnrMmybU-oz=mZ%1rO&zZn-8-kA+*dHwJ(#Y9EuEyarTjf-PXiTQ_)4$%}pN5kc+|-LWAnHOP_BqP73o|qOdg-#9 z=Y6lc<#n8^-4yl=ua;3Uh&mj#2I8j#@r01B8bl&A)}CDrwQ%FpcQQrWg4||6y ziaG*Bp}H&4L~2YToWv|SMB4vnwm_gu$eg$cQz!#FJcGvx!)j6xr{2QgW=77~LSQXy zKx7L=iX|a5?{-FP%mEk^1PTL~4gi_tt*TzAf&(VcpqPnlbWX!0_Y%t?OeC+>Am3sd z&gaI9QV`NCP7mPav$G&fh&zX&Iw+-p6z96hjJR$lSV9_qTnz)_W@`idkI7ntxaXwt z$HQMe;p=OQIOmxmCP zmK2)*6E*xPu zuTCqs&UMELpF!=im)1tS^xEaUsl2X6>*h=L(rZ^A8WjH+RaccNphaHg9-Q;QBfpqy zAmxS2A<^~_ENRRbMgig$QaD8*bEDdKOJS-^lT;7{+7C3Fmcy_Tdh=Y#c4c{ykCv#+ z04v{FBdC68FETWepX+_e_s(fFAl(jjpw<`DFSSNLO_-3X1C|uGUMq5GKzh}|C+dw_ z1UHq;dxN3ZZ1?S~O!cQ!n-h@NIk}`D|30|x!{u#TS*|y?WG!iCY3#?1CTCl5u3csa zMM4^UjH887;chU^a)xhB(?#*ec|~@}MZ%@#SLK)rw(P8^d4qVLq+~dw(U>PQ79jBr zSwg~KC^ayV(gYw}%c4mwW`UIUCW&$w8ztAOumH6q3&w?(Dw|*d5@1MZ$`f6@>AxL+ zJOIw_DJ}$D-hkx+Z|`h&I>bucZjl+IaTrfpCu5zm{LK;Srow@VGx95 za9T_P*5DNa-7T9{!u%`|;W>*I3e-SMbfp?xq+`a&=av5UW^Ke;MJ7-sl#^7)rT4ct*U%y^`JXC0 z>N(EG*f&AHSf3nfhtOcrvI-i)U;)pzR9eO&^VjJ^HM)?k2jTVk?0Mcl9IXHq5&qyO zqwtu&Qw>cm&zlCm7}#hQc&!Fj{PwBxp%cuJC&PU3LuE0+38IyPX zLQLs7?`?ScEo(1Cw zeC%+gbl9V2(fJG72qbT!b^47QfH;;aZkY^^(PoNiGoIr-e}0^@Iq8Cl73CfCX=x~m z?dQkDbzP?vFjvTGubv4%pI0GQy_l!2#EUOq$noCE>CWk2cHZy5K0W#P=e>i|cY8;B zj-rtm}8e+xP)cHjty|3>~EE&lbbZNkNqifB>@ADsoaqk`*%r)4sWU1u#myio*cx zs-rAr!HzGrwnQCQBq4`*_)KKA)t3T{vv@ra2DsdB;Bvn{I0oyq6n`DJ_-YOd8OCI2 z|74Z@a$K@wQw!>2ZCOB=dVO$A{yUG^)dUBW)P2e2lA+EZ7%j>@=IZ0l;XXM5#|e_^ z!l0MbwX@%`0Yh`m5@~f1i%z$lU#0e%1H1wXwSo2v1|>;*VL)oQNjRCN;=YJvEz-vo zl#|XwWx67N{p(*VeV6BM#)5194P(*lJ*SM~o3N$eQw3lJ|N2u|f0`S485ho?d*z|J zTH&*3InoT+RNH2me^j(&(s>96tJ2rCl-8oRW;ZI_%A9-)h!9nFBfnRwC){Wwt@dhm z^CFA_pC*B;7T3kay<%T&&9LrkH;n`qA3va$4%)DGuc4nk_exJeo=wc+)-Gw`eA>&{ zhh>C(YcPC(*9xTFsv&2>io`It)nJqRW0NHTrdXwe6v)Y(2SBeUQjmvCQ?yDNLDY(fZU*K(Z-n1t#NYuAhN!8IFa zujhsw^LwTkD$gkzHgWkXef`CYbl--<_h zmUSwKI+J!efZY4y7F{e-xT&djRPxOfy)9aV`l}?vN;FjXk9if?9MgPNJ7U&MJD`gL zlD$ubt}`-|+{L6AgJuoK5E4##oa_I(ilT}Hfz4EDZ!HHZD^jy-t2!)T4VplOK{tSR zOL=P$fj=;y>QZs%Sf{h5BTt3N>uk^5uA^K^Vu+O%(Fm&yzceN^8HH&A)vD35DK#(k zNlWL`D|rbl<4~F&$E(y~GNbc(7*GBSE|%7wbY_dT3u7vrD>QLVovOcu0yxa`IZQQt*Q2b^5-6+1Dcx%g=?`f-udKjRIbm zQm*0RQ9OW==(SrR$At>On&QMft*nqP17PseR>;d&2g6E31Yv2Pr5egK6(o2&N@j zqQ|yM8uf9r{Ju5Jzi7Fs9^_)ppqtbSx=F2|{C%_il^Q|SmhIX=_VZ$CARAY}N=F3> zfr6K`|fbZ@|pQGB|rV-?tOeC8@2=-$GXF%`uz2U2d$F)eP{3E8*|Z;pk)AC z1Y;x|1Fi`K8d3nbf?V%GX|1jO&E|m=jLVNlSr`TQ)F765cxusLyeNOzsMKa)Or z$CL1%JWe%T-ZvG+vCg=zLk#ZOr8j!a^)9rlmwP&=)r)N{Tl>7HE_ptfLmup?H)?e_ z721Z?0rG=nUHoyBUAmYe;1qe(T`zZ`?_xKQtu^z+6jviB1bC{KG_8Q8+|dG(m1eo7 zBBy^+#LX@&;(RWOScI>QA~i;YA>}?)u4ge-j)Itc+}+RP z;%G{S+z$usb~!RYF45343Gh>X0Y&~`g$1lB<^XM#0y}V1qhKju{=SIuVVvz-LKXeoq2G{L|JVoGp zUA}gfliV*Y1MKtA1f3&J%aX~$Nv)Y`uP{|;@uu@EYucgZl9m(0OHm0TE8dlI@Li+7 z?RC5tItCHc|`vv~h=g*bP^ZD~Uf(RzJ2C-Jwg90FlH4H3XTfIYEJ2tSQFp)P zf3T#b> z-XAN-zneO}(nz#d(nz@Kvo+NBfQzTxg3?qK+Y&lsDNBq2M)sJ8s(S^42qryja$lF*9vO5hiFSCU#(M^#d#{08pM9W(|z$>WKQ_bID@Nlgp*RVab&|R0?@YCnVrZ zfU1NlFu~&iL3v?n2WX$Fqx(@oGM?JFcX@2`(l}%tgKW>xJ@0$#PN`v_2*pLTz#~7- zSwd5etHtwt7p!~?$3;qB>Mbl~l)wse$juf;tHi=e2S#b zUYEN7PgcMoa}D(7=#V&oBL>Ghj3FGZTk;C#X%1+3C6@sTH z>~cW<&vBHoI1Nrw&^;9?O*3&i%En`soUZaHU?Tlj%r2Nn*G|1=p#=b&0p}z6W7qnH z!WAEoATc53$qmOZ8SZH=C7#ZCfN1n`Vz-#qs`q^DkJfWkE#IR>MmA_AWZi5s>J9bF zuY9B$D{|PJ^Y=U2!#d?LPxKtk&Q1X|>V3h6lnIuchQWaR)%_$`;_mB;saUK*K0kr~ z(rR%Hx@X-PssK()?IJZ%;jj6H4Gq99=gEL%^Dn>h(Myf?-1tgfz9e?Jo3UK`4LcyN zY!D+@X9rxhH6^&@CZ4k-Ocw)kL6cY)%u(xJhZat3n1O~VG|DgHVj_OPOnD5%#s4FY z&71RpeElj(uJur#2l-1VNC3^*#LIHtYH0hN!+pp3s5ICjTn!L)>yt_TNgmEKS`eYq zhlT!Dg*9rjd-PhFsKnIC)8NXlIl60B`4iFyPcAYDW|j07%w$`tg<0$%vn<->!-GEZMmOU>zfr_v&Jn4y&Fv+F^Xod zVr&D)#QSm)f71jp1`PFjDLbWn+lt?oG>uYJ2(jE6)+dUFVMK^#{_6LuoZrh#l%>U} z*cz?Qt$N~a$>4{hGA@_OK&~o`=9DX z(qP|<^caGs3lt314u?nSjd; z7>@cv<){u2vKcDb_4ATxqh2v+x0n3hUUQtE+WD=n4FuFVw5F8TFu#2&#>DKctx%fO z#?Zi)Qh^C~<^kd%huHgXHNSvGUgtv7(0clf~@)=Zg1C7aY&IT+B&sn0% z7!Q(zYDWfRO5sxPf* zt>#r1Cikan=4j8UY?GD>6L>p+WvNK1e`dHLT82IS+zUBz-7!ym=GxY4WiueI=6dAs zP9a=DH&BRoB7b)<-v>{0vcE0E<6(~C4LeJouvE{VDj}Z4aoi6 zNhadu7gr~dvFs<7{lu~*%f5%Gz$#g`_I`W}zko6A z8^r^YOSNRB3o<~J+v>6n2XFTe{;H#56URADX#(7?;HpLI8knPdxr0%&;-$^rr&gv~ zhK84_X5ddUy{f<&Jqfg?ifmRpVgoUYGlza$V?DIEv1gF?=5ne0ETT!Ykc%lmPCPAO`vyk2R19t{6AbHBF<&7;D=!5pk zJYI66>-3Cbp}tCyzWM@vR~F}6QkZXPQNAVT(%RO_(`uzXJ9_c%C~lP4_|DV9~wlWcW5oxxUz{3ig)$st*FZB?wu(Y^x4cSrD0>-N%s^LMFM()$ zkX0J-cp}1p;Tvlut+UEObh<&wX;&@M|V;kv<*02b`k& z1R%K40?{D!9+~TkE;c}lMO<<;Ip_kO0>V57%Jnn=SjBw+d!}(pVxBTWM^b=#9{G&X(;}q{grw9r{}quE z#jER!3%@Etb7A$sSC)A9Du)aM9{x&+a<7yihbuoIs>uR>!2!XZ*O;1Bw9@qy+C=os zxJY&7o#Gj=B%T&(cc5eOgi!MQc~v2L{`|%&Q6Bn)dJUlW7zE79QF#8`vSBMAiJL3Y zH7c}f!>q>YsT~b+dOJgO8N==jTUUg?$S>4S%NRuQB17@f4DhlOkz#U<9=di7C@39! zw&z{1yG{7myqg0Ovp`-?{T$viku}8%iTz&liZfQKEC^9WVX8RiSwDa*if2(|3YSZ6 zY%nV!5f|#vMT-_3s60G(=(_k$LSui*lJ;3&4CHv$y{>!K7u})byb0q_Oii$s?DNlh zVbLSZqR3exT~Es}ZM0MuHh=f%wYZrN7(Zw$4qD^)Rd2Yk@?J;3>4HI8!8iBJU<#u+ zR&3dqWGoMxd+34jgz16xJ@P4E9b8mb(X|r5l|rktRx>*qEd#+JQ+oj1OI zedb_V8`X(zh~_D$qPL(p$JMaxtSgHWYs;po7f*Gube1ol!8D(#dA_vfS?;SbXuu?} zmKF-&G$nyfivx+k#Dyw}6>;{8M@%^>q}XCP7O${~s3F_S*}lDgUo>!hZ-d2B<1a3b zuX1-m(i9b1@;(%)SyYo0X=n7FiE|&XIta3|m0H;pqeH9eIlSwh5%(Qwyela3OTK6r zvXDEZl_#rek5+qOn0PIyRSDgtU5 zIpU;QVu6*!huDvWBWkFm?Xib~yEeQy8%;Ss6OMDtXN>H=CMoBT;JSdwMTLK9e=^Hb zJS_TKZRqBikTQT(&b*NHMri(MrWJ2F}pZp3z7&rfG%7yrM-ud4If@_jSe=pM;bXUy=Ze4IhV-7;xrWa)I|^Kb%ilBE|AX z{u#SWK^gpW{yD;f>}2V*rRp-v|87A3U}0VU1q5kxowyer@~ZHgH`-h;ejt~2*#~EA z0UyBiKHwKN7@EAAD%z(&U~UO{N#`N@Pc^DiT}Z9j9K)00;{1X)qXxnO@X+LZ0jBDb ztk~dmmG!W{0rZh8z9}q;m9=7(TZba}@Aq3O7Q@QT&`QRL5hi>(XMvr@tWTRD`OrAegGg zIPIM3YL=-BzAKcI67jTYs|v+hF1mgr7LD1Vn*-38PijzS+wW@^?csRNxXYm2kaBZq(Cl6e;Yh+oVekBMH&b#YVz(&sXkz#{( z%>AtdDqRyiw}j!S|D6Oa8-`V7Mr*k0CmGC=!4y+O@k?>lC&>#XFStOv1*mhl8G56O zTMNYmuf8f33+|| znh5TnvD7YXTk_}`a|l@tJ?4>^V@jdmQq~Y*z*`DuT? z8sm!o@x$(+qHBn_qs}vWlWSgDH965Uf9~SLA=*3FR6Ph9%Vnh#sWVYHCN5x~ormm# z@Ua8lV&(VOA<0-fjRwUGk)6YR!ey_YKA#Rd0xH}yy@z4v!BI!gv#jO~cjnJMMVej?wmRFLM&W0*3|QU`YkEK}0%VS&4bK}+tufu2 zu_ZfHjO*Kaf_2!)c`N3SSZ$RnB&bnYZJQf6+C_h@9g+Ko2S)ZTO?(dX`7wP1_a#wXIdzC1M=)k zL%x;F{l=it^E!=IBMcgYZmTh4>68bJK?8Kab`*-V@%5SZEZK2R+nu)AhZAlNf0(uq zQPi$MAm<04C_lk}`{e{!f+RFnQ8fUma6Vh)=Q_LR8U z;5VLx|D+0CWo1RhVv|`GrJ)o)RR6+QhQfGa9fv%0yZxFVC zD)S~@iwrdmp-RitK%~PpZC@+(r{a!i2@VE>T$unj^?kO2+K6CkX;C}O9{_X5@Ku+ zL@QBcVksp7tFNr#=I2)ZR-gR3Qa#cu^S`wtU9iG!kHZFnWj7cl3!kN$7YS`dAG@)x5V&iCSu@ zLk!L@P;F}iY_6d0e2^5^RVb@gxLtvkiYdP^I$(~T1=QNr=(;I(r71QhIvVUt2kM8q-}UAtE8l;6vDPqsYEt1h$_HcR>-d1ujA~Y8Vj;^Vg4pmkZTyV zFbY}*!Y?M-JPPCgpS^#}j^jx3#NmGCD!jpOS^zsQ0C7+H z^o;P#^g?8KlzRlgEKzHi_j#IS%z9=6vxWb0GxJMaGLaxDs%pAf=ak3{znI_5&CJby zb{#=_W2#G@-q;O;pyNtkXNsq6`{&rhh*gICX)zh4>&dRIkqaw7$a4AfP8S?8Of*s7 z$?>^4cF4h=>mjN511i=yzY!F=OTOp_)^Ss$t%l>kk}{55YTN*IeJxfAYHYk~c&)Z- zHPBbTo7dB`<3VQ-3=Xo!Mpv0QU{(vl_?qqU zu$DVF+)J*0?PJFPSQHx^=;_MA+euIs-LGANJ&i%R{(S1e;P2#+>dj3ua3>rD)`NgO z|B#JpY~jI!0Qd*c;rO-ASn8M){#brx+NaSQ30E1IK4}-?Z_v3GQL<*^4J-V_c+Tksbpw^!)he%ae;APmfN7{X+ZnnrQ#9sd89;6pE8t>?7AGG( zFs7D@HlN*l%^eZ8%l2ehaO0g?NN(Hdu|zE)zd`_P!xI%n11Y)^)cIafs6h(FDdbfx)D2=sGFA<*p{mlPqOjH z$J7G+eSH4~Z5g{bO|}A44vR?ITO74dEAIS^bQPX9ER%_9!D4r{AIC!=M~P1syo``! z8FE@oWX|=H$gemhr?!7gBk z!mn?Q(4n9aNun0TNUI>tp~inv<)vH*5@gBzuIkhe@&3V_U<@XfzZKCo*^Y1-`)BPU?SJm*%hy zOf!+IjQQAXZ4gG32`}pO^Q@4$ICwnpAjXdNs82JG{?SjOfAHcOF57-TSN()1kR5fB z@Phrzzfga6Ie*SY`LpE{KhtV__phR0jiGxJP-|!5pJ*%X)_-hm?aw-acjwk_v?LZq zxjdE{+`2YPU|nk=rlPiQTHyu%^t=6W9PfuprXrKMFoQ+ z-~A{1PyT7|>Hf24PxhWXezyNlyL*ox?>+k`w)>gs==@WelIwrk{p7xygZl^h^Dr2D zeAXKlHUkNd4qBV3s)XO+mYw-xU}s6ijmT61>A%542Sm#p?rM>HcNN!Ph>3O?#v=-% zK1E7W(Nu~ojRAe}If3qG50gGv*bd=|9|6)|WL(w((Nb`~`bCfbV$Ojqv=saZoKG-tf zK{i7SDZLZ2zwU}-YYrb`)tv{78ac3y|2vIW8 zn4i@D=&e|;8X*YL3fiTT-g_AXacR|hnC#w19*^xP#}6)@HY;;e>MALHHg}|woG@K_ zN2^*uIG?v+$lUHr*Gsz-8ZCwG{4|%POU!u!g5|CMg+W4CKr_2h(m~Jr(5(SGQn|Yr zjDrnH=4w$yjYQJkFTeDeoVoxh_Uo_ki^x--|N08>ts)e)9B(2w8gR|e1)GTSR)`!W z_(Dr7XtQgvbbfS^8y#9jA%+;h(uK1FlfNu90eQ=DLWsCO9lkze$HK^2j?Oo&Cha*D zKdyp1ObkB~40}4$lG-0WDMHULW%bl0h;efrQu<4@G=#JuN)i1bPqb+J> z3x;CYw%j38*R9Y!vMVxx(ZIj=;2Pl+-Q}L^;1kS%X2A;`QrT9?oCpKy1cpCZBJ~{F zj%Vgp+TEly+m><%m&eRyVOyHYX}M&D(1~q&`|0j~*VCxNmW~HTWR>4-r9**9qe1Fypd^T+tMuFAKu|b=tP2)UIwC3XIimRf3J8v@19E*@}reXfARqOgkVdwkQ{3A1o4 zw7fwO3p*iZtGNSgAKi06qolVBAx_2cQzGLNrQN@jcFP=}CTh&ya>9Uq22Ts%E^3=&O3RtddDgM!ZGPhcG@IlY8&S``)uLiX!8D=tJcZO>99<+?h0`>BU!;7lrT@*u*Rx-l^;r zg7Ak_f?)cz@PVWxYsHh^i`YnQ7eupd=Pi{csqoDedRzd%Br)9I?|<%VsYtCDdRpVh%u`v^DWYd>sv z0`C-g;t90;anb0M&ADL{A#$cgA|!!a#{GmtXvAMDkz>SRqI99~x~44qZg!Co?GM-w zR;;q!9{J%r$%aVy~!qO5r4w%@JN zS$Zq2tdXm{4=Zov4g9m8%(EF*8-=A{3jrHFEH_pCTr z*nQmMbb+0Nmwu8h?M5v!zU+lx3Ze0;HSh}w7O~zIi}{`xgRtD{vEw=i-hvDCI9i|) z2b7L~{MFeO?-Nl#Zhcd_!w3W#RETz1fn$wUZ^5rc4^47b%5wyP9<8QRM6)y^%X;+x z5-?YBVLTroL7AjNU^8xHXehO7qrpv<*hoCPo^R$N z5yo&W;dD;5%x6xc5z^g#@xkAQgt#gfbR(ieN|cmTNZqX92IS1wumMX&LSCIX>f+We zg}dtp>~w0^-|IrAl%?I%eo;y}qgas{x1WK@k>b7`E-WuwYh_FyCd>{aQI(aS@ZW$- zknvkn$r5t^iWy!4gv0z(kgp*qxyBXOF*vvrkZ?n)c4#LFi?ejYa_fOF3FTa%C|2Kz zPGoY*D4i6iXoNFHSDurq@OCI`?(57R020^$?vXa1pIwPq*D11sCj)jNV$09~@kjxC zf;d}~3ni+MPPVD>qcu_P5l?Y|%0tHyT@%!n00Klcm-rH{_nK4MNSt zXi%X`f&j%62l<+7BWYjN^+`*2EYtGqPl}&i>aFuPSEolOTM*`l?@ZjdKCH+c^%I}2 zhy`?b8OU-FZoqc? zO845c%$=+kS-D6z=$NHxRRRcsNCMpsNNc&=+c%a?DKau|xbc^CLO>%E??n=^cwoGKLbRB1jqgfL-Gkl&o(G>2L^iKtWN_G#EzXxqvM{zG$ok&fk*c03nT5zKgxeJmIhQOp>3(3xVH1M^oPKcI$1ib z150VfZvz{ERRrZFSiyQ>!RYLpKjoNXOmveU~g_TupJ z^wLf4Pp4O}-n_kHKOJ6N9G+jDo?Nmw7xAu;r{~8#CM3Q9zb^pX-j4*L zF-01gNy7;OJ1KX3YRPVKsz4#qZ5FGh?CSLD?4-xe-<UR<3Xy*)d;V57Geqc@i)c-!J@VJ5Pr zOCtk-pXlg_T48;=fY*Par4`R%z*F*Dg#8ydZe%Y-Yomb#k34rrT1x|*-Ka!*H5r*Y zy|aQfHhySrE3>%wz8SDHKc%+PnIu=h0XwxfF9Q$_gd>DDa>X(*c9wIY^fEHFxRkMrULj$5Ax&|fJB6YD9{W_+;0i{PAqa+`t#^tZ!0pPN)nMV?}fxysTG z(>d3ecgYFYeip-VuCxxXWn?+##t04Jx@bi0we7}}B?%j0K!CLu=F|)Gx6y8l#70jG zZs3#iV|%Z5xxvBr!_nyE{P^^b2X+a;S^(K@(0w6x-Lv0-lw1Fn#2CBUutJaADC*V( zCsr$&35`y*sLl0)CQr_{B2O|%>^@ntiRQ_*C=J_s_vfvkq+~pCx82fhCg7^1^+lTW zfNdYE{44J-5S>xi_&@J3(AGgs4c^qA4$=;yYdd_jegZ(5cFFd~`7!XMAFyGHsbO(xB>_ zsTaG4vTa+ydtmPTn{C2rCpm^7Gzixcp@8snt<($*9>@WTl3N* zIo#oZN>0)J{lP9&*+gDnC5ey++Jo3;S(`6Mo;0-rtK{)d6vmwx4%2o?!NT>25D`EH zGM>*WJ`-%FZUp33tBIdvI|wOM}G@3Wl=oY4&=T7OfQ`SnivDqxdE`H_bBrNFw=zaZqTzc zU==K6qLo9=LG}$unv{y8^d?GzvYE?F8CA@eb===qK5P&kTUAn#&*W7rLvG!HcK}F^ z*FqF{wL}eZ0>GCEJabTT56Ec-L0@2RgbbaayoVdqsI2lndI;hPVzc9$_8v>Pfh)F= z?-6ZK@^6%`1L&}P7bG7*6ZUyM4jw$%8?ZMwLTi~~d55ta0fa}c$`qssAQ10Du$I#7 z=r zLW~%<(lIWUwl3nl?+hM1ct@d+-JzQejD|i0RRYz+Nv0;l1vjP8!;?QAz8;;OTn-i~ zW>mqntQFwpbM3LgA{3I`-`Dg27=-aCp(pxO!%Aa7D|31;M}5XF_PcQmt2_Sl_8`zL zOlfFFhfoSezC+?aZc4u&IeTZS3HLCXJ)uVKy9!2U+f-0pq6IR z*PH!s2Jd?@855;SQ)*tAW=2>NG0gKHu-~X1`ym{>|gz&VxRCOC;wC=aME3@8+=! zpuO4u+A2ADGQaORDYX#8oBgkMT5JFK39LQlOJ`iGDo@!vWRd(v@Y1Z^g%e@Qp9{?s zc*z?gZ~NJ$PWm}G=$hd}0BNK~`#ewk;(d{EbdWMTA(;_6aKwiZ7cmts7NAs+fcAhr zdgKP1=oB*@|DXT)zy9z4{vY@*;f+VBD73)3Mp`A}=J7X!-N8N`4+ltpI6B45wyu@d z7s?gSGL_GSW@avn!l|y|00j=?lO~6g5;y6#HVzE%5-^W83h=K_w+1XFh7RcBYx zZQGm2gRcjB`yD{dYm?xH17+mQ3<)l^58(2xgt#|*gJ*-iAv|7jo%V@D%!TWr=q}NU zh0mb*f=lw02!~Kj>!;17X(R-4GJ~-Ku{wDsXSoS#-@B?n=;ubkI}f1S1org#!R@rD zX)usgR1boI>p~a0AJGnZ0dQ#$gq-)q`#G-+Wz=01ymYqBOq%lG z!GjN;M#er6RKf>#s7pEJN%^Xp*xw&~=!ZWa8vpgbKls3I_6P4#5B>m5#s2r5y-ZAL z{~czMQwMgxNHr{Ya?NML04Hc+C+&Z2kJJtjV)x^2|K{=FsqNzN^LiJLpMPW*kDm{A z2hX7k6dqmPdJ-MAfCVtJwAb9;AU=27i2b+Y*W=Nno4vu~-NCNCi05)P7y8kogCMR@ zxv^~r7~-QxH;)I;>*d)yIt3zK!6@Mt{27MxFeKUs?!0rr#sDP|cSQ^RmAj0!D3p<< z(o0v`{(_Q&L)2)9?R3JWLN0CSWF9-N0snl+F6khT3w3#KLMcvK@ki5ZW6P+v0;g^SeF2RgtUW8!CcB zpb+@T4o~D@z=P30LRswwP&UIp>ye4s{$d0(r6}w^v%7U~z>euAg7PD9=x3L523p1+ z#1dLe<1%VWk+2;~q@jwC%uH8#oU(dW%virq@f-bXu{4;;it^6Pui?1a9|o!rc`+*n zO3&C=FLqzPIQ;tA*WbK+_RZ6i=P$nA-GB1p>*p_D9zWlI@#5*%M^6vG-aCB$oN|(r z+QU>;7FF3G-~b>!nl%q7v;jVD(Xh_=#L%S>O$YlnS6UR=^54T{`2807+KUwXxKbt0 z2r<*{!9*A%(gU`KpX+~IRr}fB_xl5O@*eJC@BoaLBL*1#A+JV1SZNuO+(8aGE%TW{ zuW~f6`xqg_<-mI|Ba?5mbkSzIiFW6Jy+HBENs_d?cvKf9Ep!!z*jGx4+N9bBXk=o9_ig&`QY0hPcAM` z-<*Gs9b0-}WgJ0l`v(ca%*cuWJD-=n27ksjKoDq=_3$BSWN=p!55EOWM{c}NxFiG~ z;(9eMi>h{}W-)K|KN2@TWVyIy3((CSEiT~M_^=AZZ08t3;g7^b3yg_JYC1sQl`~-( zQ`y_*(IcKgwhk?bI}+ifb)xM**Z}Y-Rc;$jIzj~;o|9aJr%z?+d^{i(#bM~WQmAMiVS^e9&)v;hGcsI66{ zTJRDvR3Q%wd-v*e)Zg9R+w0V!VY^b?V!3qoC7BQBvM~1lpemS{$RUW!%+&1;G1eKG zb@CKFuB_4;r{H{xv<5LN2zw8LsD~qZ@Q`6z9(bY7ORDiNK?TGugcXh88un(PT9S$H z*RPq?d9m7D-7>37w~osP)=(%ZL08{c8LHo(DxHXaqVj1blhRl@sULc3>e1knjVNBf zjiUN=rJ@>kup2%#@F3n`>&3c<4TQ zo@ki=@&33K`tk7WE$A1&K_6@1|Ms6ed$!wp|J#53^?$zqeThF0eI}#_|NPEpVS2JZ z*c(K)Y$fH_dq2H-D|T7CY>p-?fsx-O+s5JY)zFo*< zF0z6DE!O|o2ei+nTwmV`ek1fZU)wrbEaYHPNn=#rRsJuzQaUyAFYtpiz)`%{kxIpb z2hs30x^lkXtOm}{1sYZX@b$riFzE+u{QY-m#_R8nBMeJBi&hE#!0cvX9~dE@!{358 zmNS}uXj)4@w5+B)vC8q8nz0WKXXyhQ|FXr?^pw)KnXLmru7rwGhQtZ)+}iDs;#9R4e-(VkEW> ze);v+=y9G@i&zVs5@pKP+P%G#{l^k?1dsv z#GOwazT}WZ-+t|}z5^T>9^|kyzzW`BwJkr0H#z&jCQ4bpG=>SZ#A5C*6KWWW$)fa) zZYIot`J7r%dC=&nc5v7S+k)*Vml@7U$==n9e1W-msERWDyIX&kfmlJ%jU{ZQw>JDawcdy|3QcX0zn<*0oOPuhL!wUxEJ|yPSAwpiX~E(`p!DZsoiT`C(BY)D->u#EYt>FaX>y+1Z{oJ zi$dh~@OF|BU8E~!B`VSL`>;gZ8c|H?OqR<@Qv14J+#5-^^ zH2TlZgbB*NLARAQ)-g#-eW@^aBVa_P_y9+o^GzZ0E0Ku>ESk#Wi5;+WRT9)>oIn%$ zg!>MB_XrH#CEf3$&x$@DL!=RT+FWXQI;~NCWEGF3gcx+YbhC@m(HbgH@afobFDXO7 zw{vC6?+?B$lrF!IF7K_7HT%iqy|1@c_TdDjuAv@gnYslaJ*CT!8>NdN#nE!iJ|j|} z0eAHhl$jYMykqEBkjk%|yRM_GwGPM-Vzc}m88Wg1_#T^7d78B(n?}zxA$kvHXR~R9RoZYKEerf| z=la#v)o2|VcOPuXgdx5^7!GS^{98L=@kw{Ay>{Tg8Q$y-J4DPt0*P`Ra#a+8=?WPi zqZm&F?UI;1teKeAv+=u}^w9I)%}Mtp{R5tKPy64&NpGw59lIQI4_I9<>~>erqu>Z9 zFBrn7!3e)~8#*!_bjxQ5p8=tV6@@#3N=bQ&ql4|_v_?115V1lGedoS^38ByS)wi8F zZNg9Hp`KNk{^)VPF6l zmo&zeWOBFzY2cGeXBhR0D{cFC-gESz?fuI7MwH)ebqw&kU;2IT13w5ap=53z(0?K1 z)IGfQo#Ca|w<_5n6E`CJ?)c=z+aG?lD_NV$IIZ2f*^CqNLBMh9q1B^s6v~AOT};9K zd)aY10RGA9BZ%dhJM4M)>{_Ul+{lu-NGp(~PvdZ2*xR6QwNgf6rdkBJ05_fRDh*T> zaxmdWfHK|Q;~{nS;pmiwf>bl*WKC05;o!PB%+N~R zAY_{plWNxH+A#@G!VbdMk$`@B>{>lc+YKCJ8PqFX{(T6l^1hm$sz6PLkl!ZA7qT>M zTLFd+byX^p@J!~j!9>jYja0g0%lJ<|<3&`uE1P?2 z|1E-OGYg(&&d~B&=B~dELPe=%(&i}ccj6VM7IDl&+8s)CKnN#_B|J;2%=yct!ayjt z#{Lnh7URno{JmUMi^iuY)4HPo0xpP`bgb?ojIKS?(KnjuOx>=TY3)jTbl>_M!verx z!G#qQs6|5Oo?WXpjZotXcKU^w#;3-y3S~@UZO0l|$El-fK26?W> zhqqfNtdpD5z@bTwfjoVN-HI$@IDDRz72vm_@0yeG!&>1mBYDzX2&1x_Zgt`)DhI@4 z!Fmj+z1D*&^N|8enVDfkRo{0uL;86eN3gNg_QY zp>$$Ek1YteIiIn2KfJm;?9ZM)o%i+Z$+Q0Z=gHGe!s~`E&G8VBiIkSY3_IG|8MwD= z?^8$a348yF`ys?F55l^KJl5+ZvLuw+7K7OUcO0BVrl!+Oam4bIsYtl9$anTfnUU`n z|4bBOKuDLUxYx69BNSBUa2h0pOf~CG1yOi+TqlhCFI>!o?yEv*UMlTI&1C);2UXwb zJIxJBdtF=w#j!%A?A`JCC9#vRGlCp$cRxh>1oq)EX=BGlUdSOt91R~9Dot~PP4;j0 z`gZ4cQ8&U6xRt`9rOpzE5pWQqjX+7`mQXxyHs6+A8e(iT;f<;@^;#_81#g7O%J|Gm zes;gX4DJq|+jgRlYRVR3q4d(dS}2e-?6Upv=JmK8H=fWQU4?*gA7 zSr_-vZLkvjI67o@2a$b1mh47yCi0usBFzQQ$~ky`n^(%z4Lw%*2#TX!lGD)`z|KV3 ziy&7=DrKi5_Msl?pUE2l6dY<{kFhWYRGopfllQ$y@3ov6S=5n8cdX@JbBNTb5 zmm_=Gw^&X*fRTzURYMYZ>VvTV8gD?UEl22}C^%S(A@BZgyEn?xGycqMOe0t(cHfvu=)+DGxIVp;1i+ODFlAEyFk# zTFYfhZm!KR$Fn{30d9TUfay9cXrSeh{}0DaRqWKBQ(OY7CaGfwn}egD-}9;&|8>klr}vQ9If1o8HEIMcdAuE zq){37=D9+pUttpwrac~qSSab3uSDfwqa51#vNxg`!rVZ<;y})@IaT1zJgTznQY2b* zSVrtOOEG)|2MJLo0O4DwTIOl}b_&|Y78*hQ9ilWl=3=38%Yhs1WNU=s zj{BdA2lBlWLtJXIbb*~%zD0c|ry^MvX;N3A#d)h6|seXc_>>cgrw5u0iII(<;J7 z!n2GlBWbTeZ=uq|O=UMLr*sQ#3t{F#LL<26g=IIDWyPA?ncM->A4dC|S`PL%xj@BP zl$KZwO2x?5OkVp-W@yj(0sHEgUqTY_EqA5aZ(D4@e*N{Ujxny&)7E&DS<@t${6?`7p$7gQbC7(@)Wx&mjE z5v^4;hYrrqOne00F-N=IQ!8<(*>}_ZM;o{ z{STOM0jSMM2s@0O=FF%Cp%a2@ixoC7R2*I`1ljn5@vcX>cbKP@r*oA=9Mzx(_VDTR zr!9jwNT7G%;IRld5a-VxNPF3MIi!8txle{<6ne1;kEb+X!?xuh7Cd}{kscFeG8o`h zOX0+u8zZU03ZXU;y6j_+JAffT2)?d&MS{R(qKZy4MR83$TG|BPNH0yP68XJ&By zjcNF%6e96d4#;3+-BEp zpPgEd+%^K%6L^|L+JaVD)p zsPUTXYf(bNhJcN1H)L#t%>>u^0AZiiw1n4Fk@qVzEVwRnq5Fx*OQDC0&{`j|rI;ZQ z*7t_fekqc9E)$;htr}69epuT7TJnkcE|Yonz6bxm{}7Q<`sG|iamMku;c?(x+`60u z?VS{t{`@9jv{Q`Nn3>7;+wrp&4D`yF!d#UI%lyZeO5bvwiu5HoPpx`0hkTZpd+tIk zR4ESA6b$=+?E4n%-^epN_zBb;(5QE-kRRqMUoKQ--Wq%bZ1p@s_!EfI0$smU-o)tD zG^IgpM;KHCZa-k8VBEFOQeJX2hgD`1PlO~wSsB{Oeey_=Do1O7^hnx8`02D(!rltt z2d%3-yyX)kgZB`3VFbQ-6+98EoLm_Zolm8PrR1xKK8jfe)+fZqi8}rcd>SbO9oW3S z-|N90VcA(J``*nG{FW&%3Gq7T;sn-2%4ZuD&%~5oT!Hc7>dvdlfXmf#+os`Eho_GJ zL_QFl-SAANQM&kRk$3eAzHAW4_Clr0&S>CQT&E3(D`U%nyA@Pr6O~GcdILYc;E(Ql zp(IfN*qU*P8761osZKuZ0AKiMvMhyp^a$_k8it~s#_`_nlW(4W{cOy(_tubRPgRrvCdCoGFR$4a*SHvMeMonCh{jSd8O1f^KFw0KV`y= zY6Oq35L5`q^k;~E(B;pt6MrV>{hz@jlN<5u2@`puQg4b~H!u`(fLXP#zE!l6sAg4J z@RW|C*LDZ+zk}z`pFG~WyL&6va$Ua3qd7%5w~j015-s=s0aVuEgq_!c$E2(#aF(eL zydoAAr6Rw@wXDs!G=vETPSIZ(mI5pkMth>e7=FKdV0d}8ZOZd%(&sagmwkXdX!zNo zeBlyguG}eMx*N@HLRU6@4Ik3_Hf}MYSkR2{>*hR9GhxWpm-1YdnW;@=EmtUdJi9T_ zywe1`)DZFdL1*6S7?O@bpLXq4RYEX7O%WMqH}el{Ps=B;;L$029U9u44cGctg~$&J zInZLNg_#2o#0{^j7<-E`dC-7A!3X9`!P^K_^wC?EteVGf4-FeXC?BnD6)v>a`gm&F z$6X}CsCX&p>Ub^6xk?Z7lwP}9^Vg1Yf;YD;qdeVkJHXQy8nYJAAo)y#NEVV91KK_6 zHnKF`1NG#u(q&LUv?X9k#_nP0gF|VK`Nn|#MB%Sd4Fn>$GK$P8N6SK`;=t-RS`~en z*8$I+BZkK=j~UbV-?5v=gZ;h1ZeQ>)`Ex5M=G|NJt#c7^Wl-{kcLBQxPfHR)4IB4e zp>c3Ro>9s)=leVt)IzD)*nydL2z>o{H5o@+d>l^b7MeuojG`hd=6LtRo zi1d?3mY9a_6!Y!cRp=faZh4u^1$yj@z#X-bJ54d`nEeK9NO0xu;f#?00zPo$FVIAJ`?)Nu()i{Fc z_CL0?^K|%#j$SMSa92CX#;K8jjp=iTbK^TscmebTd*eN6Fs0@a1LHa(#r#)<=%qA! zknwyTq72cjXEzF_52Y|qvJ8CX4Mdw07J2_h{0O{`6U&lM- zl7-W$t4!QyF7AHHb8AjpE7Wzdr}ZoX^t5oV-RI)k2PD$3hy-?DUCIq4jFf_p=q=?X zN1IhghsV6+jJ~3W?X-uPPF1F?)NOOPriOcdzF51_G|A79`Y_4tr zEQfLS6Yxkh5o9X4UO_j}>yF|5X`Gj3!K-pUD7Z1VN~c}+Vs2!@4y$s`La9&asxp=x zcUnrLu0pB%cV*fx#$gA(y*MRLVt+Y*@-AErV{mrKjt)P%$rbnnXO|ch|B=nE#TW>% zWoMTmF4SDh!lB%MWUnjG0nRP~C*flnwIW(qlkmGcxDavd(uov8_nicGcyxwR9tqj1 z_HYd+>#O@Pe(}CgMxqs3G1N%~e#Oho_$;k_Ss6xK^`*gbs(V{k? z=aIq5NmXu2W!@A$wqr+VR2hX9DMazu7Vbe@$+MNhHGcG?#(W_kFIwZvIe`2(j-0&_ zA)E9qha}7}C5Na`fg3nZ1O~tm-WxjLp)YEQxgtusP05lxkct|6;d^AzI{T|mf4rC@G3 zUuI=#3&?1kJ@|*E0a@Py~Cqc|d$v*Xxz3;}JbM(tXw^-qyx=KZ_ z;23y|4~LN71FNp0X{F5H66?vYI^+?|6PHjVRUqJ@sfuwI`wO*7K3moI-NL_J!jq$Y zufDHrY2ynlP#)i3p#39mFPi8=F`puE?2JQ5_IKDYSeDRYXDL`kU;xc*c-$TB!#;Q4 z-34R7gUheGyKj~QaQv{xbNmZKXZ@{jvGVs@J&ywOZtGk4>-SrKX1|UhicE@?$4C}! zP}SPC)35}-k-+zki0n^frnJfLj!(wWstfCLFj80 zj%{<3L|)L)3S<$!k(ro@l>D;9dw3@JW*01rPo6yP8V5MUB-dsD`e~9$k(Z}ks_<(n z2%8tnF}nvgYZ4tsHf$C*;@wTJ!4s@C>(!<^TU@1A)tYPA$2;1(`(pA9uDh9{_CDi4 z3zcPU!Ts@W7dr%w6?buG%U5zCR2BI!*4S)GU+>lI{Vfdjs`s`e)T)1Wi$SgR`&tGT zRaQz0h8`qynWY*wthdzv@++j`m09Xzrt2y3d$whKbm6SZ)e;TMIOW;T!~NP!SyDd#R87&xPv$e?9%hhA9;tZd->bZWeSxW@d_MX??a)h=}?S+D++J5 zEH_$KTQE!=68Bk%T6Z%yd6~QIosU&LL+_(U=c=rEsZKHGKdmsd;nAbdLGXgVKPC2I zMnV%Z56VgVySrAJBapx*;{aV4fki$K9l_PSB~TIx(e>lPsi@M`+2!8;xL5o4<8h~@ z`u?QJd<|&?HSzn9RtTYwgf8D+oOTL4r{4HN@iJt5yF5I%Ukytu?al!<{*_s?CqDzl z_`}N3e7FMwB(vS}m*=M-HT||^6>>TigHl(fJXQ-XJ0DL*r!QZgFet=QD0OG&8>n5j zc;5-eYjv-<;ck_w(}0AkP29~!t=~g!x~uKHDtRv7kF+$4BP(U|Yybv#k^We8AfLVj z>mU2^HJCJi937dOk+7;{dcd~6y(yBu{rmS@cUS~F`!8g--3dxDDh4_%V54u=>fP&o zj#v|BiSmW=bzx`SNA}st($+^D;WMwP& zHioQf>9gBKR&{h=6G>M)Yy58Nd&CtCE2Bj!jl0fPB8gwvQtZfa2J-KUpLvLS^4Hn$atm(8N$ItM#gqkvtDiF(`2!XH$4zD|z3t0BCq!G&I zrR#AcvevQK#!Mih9oB<9F81%_f4@yY=c$#9w8ypg7mw>^XEUnSZ1=bB(`TT{b+ewT zvaKV!{X$>d!?4O@S|dC)8P$p z?1J;hKVoc;%x8x#Oban7#^~FC5j&VtE{-TD6<9_D8^0;|Uu~-pVqHi3C^Iu=6l@JK zDipU{Zwe8sw7}TBMiV4&^mzg(_I(QA@r~9aQ}8QTMBZGTJbHA%Bp^?4XTa}+XF=-G zZF-Dn#sU4f7|T`!#K0S3B+ZD7V$fW=8O%43tJ1lm$VielB2TkA5>ovN7Vp8#ucl0B zt#mDGS&{M5UZN{+=2Z`W?rIt1e2sB@cK3b-Rlah^Y()^wY3Y$>t#adoZ)*KQ@|t$y zm!T+=A;s~d{R6=1AfXf-8=$%QSXyeH$b1%|s9$NGkRh?-X)?~edWCg;0yHbRR+#Tz z4A_+lNlsu^r^;??Z~=3@)^c6O{%#RB+L%`RSsban6z|JH!S$-A>G=L9ga9~t;6qLx zr_K>mrYv7bhZfmT|iQpiI~9UxJqC+mMj^N`XXDiBYZ~Gl{hm+nikVv-&bM+9 zi2VT0dmjDZ>AOT38f+uUH zW-ODr=z%*Q^7+!RIX832R9iE#V;rcnu&3+OyPGuy&vT(6MSgFqP{w}vWy|tuOt}yCOhn`K zZb=?`v#~8%sf8G@llL6r!&Q#g541Y&vGFV^5Ymj_nsJmL>COoo@LMxL;_o<~mya2h ztLvebIyjj3>1)*ClO8*x7g`j!A1Ip)Iw=!$9)vje0tpKbcV!uX%wc}8qs@p)P1h}d ze0lB$|9W}(-_9b3Sr6X3O5 zZ$rd5k@85uFO*9(ya5G%?{7eXDn#CP32+p+xw*URSyF7aBY!x${NybeI{Q1?l9ni9 zA071ka5M`2zP&j02b5~~UD(>Lcl8VGWKeYH8a#vO+^Nh2<&%YSm=dv*Wm#p@&PBSW zVqA6fL2lTf> zr9Ecqpcrcgb}P$V7y~|_cZnoFVOe2?3Oh1DY^@J&!=46HE;D;(+c)&60)LpOO#@*N zC`Z^4QbUxWCr+csOKGMO5L|Kxu9_1LF^A+W5Rz%><|*Z+Ra4g>VCHOmkJpA%&b2Ue zm8EU=)5t_)pNPsBB`SiCc`p}Z*5e=$zgB9-+0KTbZ!wZsL=p!1?Ix((3m=25A z)21Nj*s22`yX6F#5Dibcaq;%{ugQ0t(G^+_Z-6cycaxoY=%h!>;-YlK1P|iZ(8?(^ z2Pmt!%{ulD)$s$P&JNhtx0TMm-vZ}E`!9R0`$_}mp2*9NM@LLvlFFHAz?B~5ZfYe%Dfw)Sxc_7FJaAyYW5zR;&j46QmdvPN z*Z9gfH-)|rg3HuQChR&_wm0Sb3@VJItju7COJ!YP?zgtylJ3!2MbQHq2 ztBqOH01(SW3Mc;9O6mdbQ2fYE&B9y@Z(B4G&tY zvcggx7X*CdyCfFKXBQGa|4W?f%#8pWg0$Gqo!N_ILER4lhCbyLJG!W2l1amkLdJ-g z&XA~jb2#+cIJ+^?>!xy-yBw01@2sQY?i9;BuTM`qdG^}oUDmeRhW4??u(K}#!HcpY zqkm82R9}1dtanVEVw7c%W`mmUv0E|eg#>atgBm1~MCKG9pU|a)?2?%5#78s^|M|bO zV-@|zma5VbLAL}K#`7gBv?^WNr5m9sXsd)(f|k}y+a?gmwN0@G0=c$$-T22Vr~vQ3 zc0BBemvdAwx5qh?zYW4>|pQ5JU(u~pOt~2g$@$;tw|vRxoI(nNgb5O!*mtJpvfr6_=gy~ zFuz&GC`S+Oai0^?9&xZ8Ra#uNZ7z@92C5lF?0JLKOH|SLV@)~H9B{gGZ%0nT4ODRF ztWJZ>Whr@fEHd6sF1g<(!zGn;mJ;e2z=>VAQ>jgfop*IvbZ36s+ufu$dTfg(1N!Nl zUrOQCF~57`M%K)@L8>Mq!%7TFA0{@W2i`inwp5wHvm0G&{`Z+KlCIyK zOAmJCyrKN(GK06lgyVO>dajiWep8OXYq7+qyWZBkEIWy*cS;@;TL-`V`l%f{i~nJX zi2?+*8u=vC`y?Tz3-%7)9(+ccki2F^5`vJshCFZjHBa-Qn%K@b=7bsA#``=^`-uHu z|4BKVGc_Byb?!tXBNr#gGmFn7;3{Kxi>=r5iE4Z+$XN%WdDu8z29!%t(!jCI7Z~+J zRh0cq&HC75m;~6+4KtkG+xTJ*HxnFjm!=KMV{W3Wi^HRn9y>mH@%D!vv$cBcr^Ac$ z9y__XcyrNXFAuK{&w5}v86BRV9)-D~bF&G8G*qX#1fPb*Lgj!xH$g#XNxmVXne z)ys>j41Lucd+rZD{H753mB_?GlzPcjP97NN3U&uCS7g15pJ}M`21%ADd<5Y?0IEq$V-^*1NQA)nezLCZwsZ%?_;M!K*6;i51u^U`+BRR zb?-UfF|b2)DbxZiE{!qWF(jvOy5@Lw48k0r;nsTE;#P}Hp5XI|&x?~P zPaW!ON1rPq78<>_eQs-*%wStW-qNe3kGmF2`aOQpSVhcQr(E}x(gJV)bmv>9#f?XiiTeG) za9BI5@CX&3Sf5HNKDgfuZ}x`hnA+;x`BAP_AzdN6?r6`4)@c3X^Sx#kx`zIRPi*M( z@8HCGV*a5{tf%^SJ+W;8v||IijK;w+G`OnpP|-R6=05RYvSQ1w0^Ib{UI?2*-CpLH z`j$NEYOdW;u0DHEWMT$9O;5>2ur8Y}6-W3EXH#$^Gfnq~S`RP`o~9?kNapkOce|{+ z&Qy-JD%Ofj0JhlG30N!iJvu;>&}Ji`$-YPH&Pa17=^rmbwz;3{zugI?KY5|5JmuiE z*G^T8Mc5>i0T2>tN;T7bfl%nJ1$j5v-F@0)gWWy*fBW!P`+xSI@UN#mw%23(_P_S8 zyMJzz_enCV8}$98)~^0PujJb9;bWxN4M(?`0NW<%%Up>o`u+W`#cqQB5w6B=%72gR zu|~-!3AOh+!9lL=BH&BNwkx^pr--<&(T|aG+eG{h$NKL<+U+LjAMRNHu2OG}lA>CX zM-D0RpLj{{iEbN%^GhV;uH3)DnRK_~M14`oyVmn>EqxbSEkrpNmHEvj?o$%0|9DdkQqSSJ~0UF`f}0CcJ`z;9pQ5EEOdz3%?J1m;m_|`Ju09 z9`B*EHShXx-?{)rde1LaXtmF_2jTwbE1<%;g1x!uHhTb%3>fPjo?0+kqH9MMc z>$2YW=&sru_zbaBq;Ta2nbjwZA(RCx%Mt&3jjMB4zbn0J?x;x3f66v%w)8o-S)*;7 z-B)E#P&9x($W_VsbZTcdI?Km~nJSqxZrHfa zUI`{QQtx&^C!mPo3Riy{-I1%A7*Dx9ql;IddqKZFcJCGzKmDEVD61Q&5W z^EXGQ>^0ZdqAW6=px@rig|XS%krRg*pw>K~D6oM_%QDdA~A4$+;OG zVXWQ#t;W|_gfT-1iK0k?HYvnfvw4hZOBN0uOVnol)1rLK|^ z44-lHS`=z^DzVdgOe)W9MM!z9s2Q`?;zSg_Il3G%Q}O~Z5q*N&#>9S%cTAa?p(SD% z$UzseROmN;`>$44K`pk_`C9Dl#hLd8ww(%|I?I8|eToxrW~kXtfQac7{Ly~CSqT|j z+k2-kUeh|Q+<~Y0UwlsLP~2s3gU}$4L%jMJ>#IMxPgQi zm%>sGY1#y3BWtHpx)415Ny}2a$rBM6UajigqF`Jcf|rhE7n68*Lf?WrRh?1Vb2&^@ z4z{78L0F67jmoNpFvEw?bSm|Z_mPE3i+Z;+LP|{9rtHog9=C*spOO2eWIqBK>?q@M z0S<9!*z6ueWrXg}Dw&F*Z?x>Yk+^7Kdm8JY0}U*3g8`s~d=-DV5JqKS^!nD`C*Kcv zvJeBBvN3Zusn#64m&Ph;Fp5>zU0k6Z?3MD8cb9F){0~Eirf!Qn-GrWn9A*z}Hw>rN`3f3Rd7nmCi(Y z*ERNTq*4|tF|4)rm;|00KK6MbhZuj`_wUNXLZ$uGx80vW+x?q8aW5(fku#a=!FWfc}bZ;2YpI-0O9M2 zQ-|p}(*>!l8Y(3H3Vzqm_jbL9ITfI!WDuMngHqjYu;!9V7sS~b5IjTU?K{6%>>!xc z26uMipLQ6GEzGk_B~B)2Q%^7D4NU8!QFwE)RQh7uS4X4a+v8Du!hI&jdz|pgS8v{) z9kcT{SMlL?5nF$chr2$?#^L^E_nUhkZr7mQAE%?PGw{~n+u7-e$teP6P!4>+sE7jV zL5FnuW~tcj0RGqW?^8TC7#f$s%;3W>6{79hrgW$Zop9@*hdI zxEMJAsn7zJiaLU44yP{pUI0yIuwpLiGuxO7_~<61U4L^%>G+FFeCxB#^0MTRHPgK9EEDDjQe|+gaz3h(Qae&DjirX!BAr`6> zhiM9rdVlQu7VO{Lo8|V+FeQl4{lR_@tU*>4U+`-%04#WJwSDbhB5ov)L(B*4 z(&~OjV8VlfYrYVcRUNQW^%(j@^`e&S-B)M{?m&Z2wUpIPS?yC_yyuG|6OGxjGGFKA zw-GN`Yn~xTgMN;3sLW^Xx8cKbS%|*v+fvdz{=cc_(eB`LX&vpBzlg@s-Rxyzsn&$v}qO6IeH{h$B&zy9z4{vY%*B{)1n*sT=>R|2oo4#zqMG{gMvL25VE z#3Wiq0lA)H+!X&n;*2A>C`Mi9#fcs4?c2Hif($@%L)04@bocQh z=lMjXB0UNN>R{&|Pr9>=4{2We1uy5rhtzA|&tuDSNL_8xovVqMQ`Fn_+-%G$OfLe`LLcrHu%HwQ@3xK4kA^#q(+ z>NNs0>LkuZ3%F;&)o)WPDq|Yd6>c;uYqg$K(bf3NEuSjNzHP5xX6x3@((2xf?=v&# z;gKjUJ_pAOt!=>Jp7oK@?d=TMg#dQ~|H$9TgXzULM*DQE@(XgxFvgA9<>LWQZ-g#o zODO12qTU{l!W)OHW@T6V5wg5L4$(KOud&A)cmq&tB*(|*q0*R zlE2{WyW{Y9XJFMbjnJvrxw_3+CKuBAaUfi~22Yc^S)DIXnIqWPqY?WTnDgy-K{kE( z$>&jaPr15zjy|5X}O2{VLh58)r^5LWh|e)IM7XS-iN+1uGUKwSj0c3A<}o(feV zroIctOe>x_r{M0@{W~;x5M{r$*`u?Y&uIMUZ2wD{Kvs7CnG7M+e#0EHPAmCLMv>6k zN0~-CtA7y#3HAItm`PmEpUqf8)>&{bTH)CPJ})CGh=5sIh(&P^_L0m*T4gey^~H?@ zTQvB(xZeAPZ_s(>_`JhYnF;5q+F|2%LrO~hi#ir_vZ9u55XnD3@!EDL)s)z1aq-ay{<-X0g1G2d^oHp#ssNR6pqGMYrIeY$z4d-|+vIW!R zu^vJQ>?w+f0V#x1-)p$haRV?Eo*wyzpmeWWc9o{yS#AQ=X_0c%mwBC zML{TRvXJ}AWx>6bDL4YZL4^*6cy@US{=v3h%Er{UPtuoyjFr-)K$hs>B zeAkFqC!jb}JdIt4YP)6?L#(wOXPD@iy`NluDgWL|>3(|niXTpJgp|zl2n`)oaEME> zdmP=2?8Ydx0%+WHz~0zHcq@(QMGZpYWeWEX&Gp!v-%ux*uLD}_F%mJbeK)_2eq4YK zD3L~#QGT&Nugcl6$ECNM;MJ=17{EVj5SFWE`L4_7w^USvSkS2?_Gt&866*{1LkSvl zAEP{J5KXb7KHmRW*CEOPXD?nX+wL{tJ)`0JL@_ewC}l)v7~HZaJmWd2Pz#lcw#(BB zN(`ThbG}#+h3>SoL}i(T6jrvp9Xn)sqld;2A~Df|V}c^{JMPa!r^APqP^5F`qu8iR z(mkoPX`3jVu^qn#Yr98|iI(SZbn3uS&|vuZUw7)MsP=Yu?-*Jj4Z%OrLDG|Exmm*?)vIzKD=fHo9EPmIv&R{gIqF$6eERnj?cm~LTq)wv01Uf z8LHDEi&Yeyj_%txs(XRGf#gqn%YY;2>4;`}`*d{kgnyA8n_n*^s*#keWwY7K@T}_fA@w-`U>iz$Q zR+~F1_^d`7VeEnPV^|>oz7Eb7y$zh}W@$O*vJXNAUuyNj?AJ^!$%qkAwc9rtL9;&#tb&6|aMslJN+3^lM(2 z9;39~JC-w~-p?Uo9L}IYP_K91+Y#-Txhx7%9@g-8RyeviuMN=4Q*cx78>}B^gdI{C z+|5T*mIz%QR^|NV>eOt}`gLMr(~G{Xyqzn=rlK24=6!kvBNcg5s-W--l`daYb`dTS zT&Cql2&Z8JMbM)eLX8E73ByLb>*hqIOYhzs&DP$t$KO2JlrEK*T4kBg9S>XA-^9sy z`}s`f?+FVUcOlj}@@+Ul1>EGquZEY>KnDEHXaa!NS65e~6RnkQU&i48Tj3r+Yl-P3 zDz(b)Tvb_Wj|IZRvV>=ule%0i2pM)Gy~%P|+aW9X>`vVXNu|bKU|Wubbv;|v86_BC zkaBN9w>!=A9-c4BE0Rp;AeL<>_Isaa+0KAn@oO?`0%YA+fyLUteYN2LP=3F>NouOv znlO{tRO?kKt9UPqhQ7BE0UNljw7D%*-B=xP9j(k4(6EyT?WIn(tz!%|e*u3K513_Y z!m~rvI4>x}=qsN>pj)AJ3aZaY8sZwjI0x_Yh-mD-)bP;mL0DDJ-|8+5&$kz6J~M!` zBh7i~8a9oT#y$%#X1*jCmH`_9HZ!iEt#*R{lF# zfuwyaCR6sFJPxbqjw zl}rQ%&g3*)AyJRWwu9GO{hFC9k@`~2P;LWxEUHff--y}*j-fqdQNTnEQ*0uvycJ;I zF-wFC765ORqR7Z7EN+BevYE(*1`EvT$hRM50pD;3cMvy7MFFA=2@DVeeeP2klRGT~ zCtEgG1qg`dS{CGb@xIhN2{|E9jyN5mjm-Yk9V9qlyWvRyNa#dDk6!kwkH`KAiy@1fXP4C^7iD8b zPDeM-n87a~y}A2-rd3to=U{RN`oUYqTZBNyw}Y?EJhcdofCIutlrL0j#M)F_biuv` zd_5*Cmk^N|GY*gb{yTQ_c(A`a*zF5G_~w2js9SD|Bc%nv^{xYO{ps*>#B50hMeo;* zQzzON*b!ckG*AsxQ0Wc3fRkm#_qBriq7jS8Hx}73B5}j>gT#Ei9)Zu+F2nf>lD^q{ zzJGsuHUcvWY3%|PzV^pyeGv(U_~NQr2^rwAQ&XE89%u@bY^qHG6-SsvC?Iy%$v8x4 zPs_C>+-8`4zRF9P;d8Z1K?K>tNg$=CHDO9l=9>X~o69m2#`Ns+xoq}<=x3XuUoiAR9f?AjQ%h;xQzcL{)S}3i5rvQM zL*a$s>4y7pBFbAKa#r3dW-18pk~ad52UCz#0fqU`y7?|}Sw5155NrJ$9OP2upEyyN z^Zz%QDKy-!wvD&IqU#mB5n`%U?qxwIV+YB<7g;Gv(&4;a-(DwZYEjc>#K(ZWC5!jp zi?O?N;{IQ;ch?6OJ0Gl+faKpq>I6`>ZMF2^xne}YQF)nw#e2bb+#aCdj7A3u1jecz9ulG z*~xpJl+n2@b+svLDd$?1WhRd4k*PDg9@44BR10JG0ajtDud`84>RNO)AMN3Z;2LJj zmb}}%jj`mE3{gk|qr}>1?X}w|vY3;hh$z{FXy={q2O$cc$s4hX1IK0)nOd$v1uf~< zLKJlv^=j)gL}c> zkpFd8?li&MPqp{^-*_Z&bi8vaueqlZrUCF9T<|~`dJ$rd8ZD*@*jS<#1ux;Q#9Rwa zw}Iw-MV8c6?@>`)sA;dcMmv_;SWa*bHQgb zEoQtFS87#IIV8Q9V#K{xnJ{dllmLWsz^ihe!~g$nXnPz!jN0pCd!Cxrt_wfWeFgjA z1(?$g5z3nwg1<jfGfJ;1uL%^G%ih;5zC4ZD^*3K z53_Q-m#dgfY2_D83;$qd_DRIbb#45Pr&u3gIfo_dcY)}76~{FLANWH^0~UCIO3_V_ z2ivI<7)EEjfH*YZ@}<1J3Scc&GJcMxIfnAk#ws93+(W0+l;r}OygkcWyW7@r+Sygk zoYo2i_EmZTZI1nr+mL`G)4)9entgfKb(d!*^bHH1kmINp)83j9_&zi zl~)=J2(5@pa062xniyU*mN%DpN9?_pH#0GekSN3l+I_Jx37XP^;^c%RcyXb)Gc%9J z?RjVpPV3Ut(7eF8k#i?B=Y<^IIc7$33hLqdXom+O!^!*71_un}2C-5Wbb)wIQ_v&Z zJ*v-0Rl{3Kpc1?mY}JdCGI0*Cc;;2$3U*yJN~O7N?=vmh8(jG{wTf<;^B$I_q)v$D zIK@*^Rp%L*0#MO9w23PL;ld~!oKzgi+(V?ngbqAZ^ zG`Zh9zWIn=gQuiZhG{njaQF6fET*|s$JV;q9sBvg+H2kd!PdAYoJPJ|L9oinVxm_U z+BrnNT!&&$)ll=u=DO`-_n8ZeN*R}9obJDH)W#skBpTlrk815?}>4V}!uuQ5mTr2qZ?TQ9fXFdV;9sKBadK4qS5u?C}KI;*%S0buoa3_R&k^(S6TPeWNQmQiXzaB*Ox9(Wb5UYT<)di zJcQNjs)C&=aF#O zez}mZA$YHIuL{hss{4$L#6@84DC_rxUK4SFkIYp*QgazeK4bq+GQPL*vz&I3@OPOH zIG1lOkbWwY&`4ntbM#A<;pO=JgfAg7Xz9IvQ(J;Ul*dp~wMaPA`NZsyh*~Jehh4N_ z<)b7~#g?Q_S`j~%R_g=2%7uuTENT~=zTawBTaj>?vBnJ)`OEQxNUg0%{02+vtM z)zZmGB@tHhGszh{`MxzJeKqF3JpsYkou<;-)!`jePZmkl2Ni2=CAfw!Ym3~A za8{H#iIu6<=XPqV26ZNcW~yLFY%8Pdiz27v60&Qb7K-7M+aaAnE9qT=riH=u7# ztlJ^hJ+p>W>}IFNilnpb^act`yRoBDIL9Q0Zq99*XPpjS7nPb1(NtI9_l-4!vZx@o0s=$eq;pNlJ+{X}v) zS+uRSO|`Rhr-f?LmB*8|Gvahn16-C9PST>vFRl)5PNq1I6EHqY_Zc1B3Ux6fCe3~R znTxdsKr*M-Vj5Ru5e7oop&%HlY7F;qg1^7KJo8VW{|L*vVc)*o->;qKuLYp&Gpz|? z?P&k|{`KL$xo@3wm>Ig?!NBX61U4wnqbdR^9lRIjdyMg=6FK>S4r~48P4&%%{;v__M}~i z%=w3S*D4JNTy#!i96=5w7q8X&q+rT1QGC^$&OA%Vxd86(Xp%5&dvRP!O|>|ukVUeQ zL@vh>*|pplw08Yq?*MqV!@UE$l*`^hV`wSjw~}O=9Q&2}N-9Z>l0%FooiDBaHA@F$ zNUs1B29Ps|Dtr&W?#=P-MVU`YL!BS=;rrCe$x@3baD84#cPIeKp*5wF`W`#e9*Qp$bg|btt_)K$?3K393%OS+^1`nlrD4NQ5>6R+D*x1 zdXJ$Xg?f&v%u{1QG`Lcy7g?g@@D2mjugYNmjf%Sa;)~Hz z?Te$bU{y#J?6a)BF((C%ae>v6f0>bFK8jhy(e>fp;nTgx&dMJt%WoKq|7+j8zOS-s z@O|~&&KLjm5u277qdeyOV%C*B`FCkwR?3fzB{ZT+MX}r%2L}fS&!0Yp{~jD1H2(YK z+0(=4|8)55@$=_T51&4H{`j8`9zQ-feELu5;I0;Jw~`k)|EGhs$7(L_6KMkHaYlcV zr0ylc86~#`NjdnDt6===!!NFA8oyK&UfWLL#)i}Ug+3iGzd*KzTl^DQww2M8q~KIH zE@={z96HxSUmxr}KHNL_0s$d>iQMQ{$I&muoh?RzIJK9k@lCxHgPfsptKO+R>3(5v|vhliM?02~LSX4!){;ge)|% zer*i=`>6H|Au(5VkH!{71`EjeM8AUdUmK0Fbd&&!k+ zi+upM;IdiH_hWKRl6_96Bb-MwT9BwHbFz;!I)a6yGLmB+|H&v@Yr_hL?4@FqtOtPP zOKguod5A04JeTyZHoKs&_w&~mmzB66r4=b(m{(hH(fcEyNz;jrtT*w$Mgw5Q$ntUw zVM`W!-t6|ca+Cu$Gj6_cnxbPoC&@A9Xj13dTE~&U z$cT1jkZw}FCm(rFKav#S2eN4X+CPt@v(q2QqMC`ANcEd|uhk`TPOW=-qN$RwN}=z1 z16cy_S!Z9!zj21 zxD7JoM=EQ;3pm?uK-5N zS($b`DvO!CPgN3xYK2M_w{;|SlCmu8mVb>1olbOi3-P(^6z={xGvc)g=#ilbI7#Yh zi)b1pWvpHQa)RQVWm)?+SPHhFa0&z{=)T%J+|XIAF`fP+>p3&c)5_IBRr*eu<40{4 ziRDo(n7SK0^93KwLaMAZAK+daEy&z^n8U6LaWh!#s-h=8Q1+7$glO!4)|l1liQd#Q zrT-`iqOl;Z6RPr_NWFF!dIuny@9wX+Wp1h^hHfnQM}5&KRZ%{sMUHdp2GW%xJBPj^ zi`|-vYP@J%i2X40BD-vG{{?U4Pr%Bmy}bDT=!e&%$IqUl8Rj#zE!8;BpUNB*JDn?2 zyWWF_C=C2sgYR3$2vHwbFp>c1WK}vKC|`gqt*z(&S&b^*p!KX{$T8+L5;wx~?BXl^ zsX|Cy#8~#(;C37;gy0 zy_|3-sSdoK-1yXi8UJ-tERPfa@$+R(l0o8!x-hdb86rwB^?{`aDEBUY^c?#Cy1C-t z(ZoYDdwLfizt5AEzLgeZr?eRGHK)^*rc)VVwtf$Pb1ilvd z$JHRzvFl7s@>Cp%We(Xp^HoD%01wdPy#sV9=Iod%d?mv+S&)7K-Q@w4Y9yJAIDk3! z0O8o`#Rf<*^Ig6boRlI9{3SUrWik0H$o+2vYF(*#)hKO`kO_w$8m)g|9){r!J1ZD9Ai3 zWblrRR_j59YSlC?$P{b_&Ub1tb^%L&7Pj=udhA~PG#ChAFd26&+#fPIrBH^V%uw&K z2!=({2>^R_(rZXBN&HjC`~%A=PqTkEA+jezQKyvGRAC)S?TeVh@sV=kGDiPv=@jLs zM7rrDZkEs1CM3tUgVqvD>(oIZxJ+8^U;K(@-1gM6y}7PcXuD7AS+(!Pt`Zp`mX&$N zpfXRIEOt>Wor)kA6l9=?%c#H0zB}H2tCL>A?Qprs$oDk$Fgs`|KwjD%O5@H7Q{Be*ZM(%xBgOYkEx$5nXshbB7!UIrTlycl^|b)&u*w=D)Rma-Sr(qQ|x z>2y-X7wa%@h|1APlcm=()ye9Ugx%0|`chj`zEyhKZ#yjmgK=WDc?`yhKelnAOPgp* zXpYAg)mcjHrE#>pR_Wd8$+4wmgn!SLS*L*b3{^BPG>e_tJ6MHa!u57u2L;T9NqxXTh-h0ImSawMp}a7Bevk zJIA6`-K$H1p;p#o1V&+3ni+nHWytB;Y8d&8T9Zr_-Yp-iB>+(7C6~@*0GYcp*CTXm zr6L>6aW+zjU9dTgoS4nq-j+_VT*x=oRi?h{yQokQj>+hC|MmoIa)w=2z$H3R32OWL z=$6Gfo|6k&bYNP*Or&;Vor&~d2`_L#jyr^k`o33h>%AAhD1qYhL4Z$T?|PWizcMy; z+2KN1RQavNsHz|SaQx~TOedLR>eC9>_TV+V2tu1Fk3(Yhy~+lXI=sRk0>4?I-um};fVUZ3l8Gv+TR*5`xlQqv_|C$)^*p6FN0pp*1m!yP!&W9+ zj3B2s)X%l3<(oG@nw-}lx7bXxouDU;QIr!1f5AyT3A`T2>K2B)#yMpruZ_Wn&~np; z(DEU){8I}pzl2On9FG2&%-TI2&FV*;H)~0S=A6Z3 z#gK*$HE3<(Dj!;>hcqIam{LLJP{uSsBJ1vlWoK)25aJpaqujE@c z5!62otkZB*J{Iegld0Es)o0O#tkypg&hfO9*!y(#9aD?gjJV*|@ZX1ON!E!{zYGUz zI8cKuf<7a@wBun+NOnO;-~D@{UbD%C{%u)8u?~+s5E%;=1CbesjAPvokH}2PoTh!c zj=#c7Hi1#UtjLe}@_V)h9=6%pzxVhIG}wa0K!aDI!5t6hlovU6;8#6`iG0hZ7W${7 z(}7|%+dwe}it*V}43;E#?#M|~!;g3Hl1(qvFDufDL5LZS!zZSt?>a&u#>loEay1j= zaCC;FGsyp@eJ*TX$HRa8?1Fba#mmnmwMmQk_dr;JmW>9&65h{1SZeK)CM-jg%+PAB zGyW%|xGZLCM*sB1|2#c>^5mcy|MU3C!4Us*AMGK>|7i4EJNoDB^r$Rm@QW4uBY$m2 z{-|Lk;j7|)03qLblbTv(q8mrfNgD%@A($d4=0ZqOjSLjg9(@ zT$eiPtt{BhQZG|f(3^#&U2X6NeD3V*!BuglkdlK4{X8QneD25=tCI#p zfaBIHf^(8@>C~_|jnNj)=-;o%Vk6NQt>L8rmXd@03yikoD?(W$u~Qvsg?c5lWQon1chHx3dlJu{#L*h=M>XaktuJd zsJ{$XK+cDINmERnSGGc%EGH9si)d=Y07!OMZel?is+7dKCP0RBuKVD+OPPNGZGA1@ z{}z1>vi7%~xsTta$pYalOQ<|&sh%0-;VF~@;JI!rsAf>HKEdlHTIO@Dy2p3OI;7fp`pGnkfwEBgH7_4V zHC(Xp$uc}Hmco&Gi)*-gf_1gZQW~fIc&s|&TW?|sSaVjP!&H`I$lUu1in_9vh2;X^ zh5ng?8nge(9wuy;BD5@bcZo<>s?Va~O65u5e<_!tl!k{syCa}F7tBcb;Y zG)%_yG%>_=e{ms&HMe*4{XGQ6b#No)h@)T>Lut%mriU)FwCK$|xu`NCK*h8sVuX=?En;;*7JCIm{Uc>J=Mm11qX7ic z4bsnf$>QX~%&Q>dsfDytz_bxFVk$rh%LKM<5BlapnqVN7uLW<&5Om7EB_ zzTdMGyS~{gM>hO4`MK zx{8nE#R@3ho3Gk95F8b?koJP-+l{1@n{G(Zo`0ba6%xH_|1yun9?NH_bng;=D@#36 zDu)_^sHjl7w&=xJRdUeCTUbNAHmNFMC|mJ`Wj;-ER^q5}{(FBs)SgrKq;Vu1#|ICn=?`rD(yq}Ycw1su+ybC-|nu7xqex9nO>N7IAc7}9x8(-^bS7r zuTucluBok;D48lHm;AQ4SKnjsljZ`^lNhbcT>q!Mt8vP+)XM(dD30im< zIMtA7W*WqZ5hyvZF&%MsumY~Jfa)sPSE=N3-xx@4*nMcw>T&_~gER_zY>Kp*@ZaZD z?&>?d=3JLL_?ZiT{hLlaH#AEHU1 z--%Ao!biD2iIAD8Y5DiNEz=f+&eQ1@4JeY4-k_D5Mi}2^XOuOC$uOW6hRYZ1fV!9d zPN1`!mpOR2Z?iyTt@@@Sb_Acxe|f*^C7|81qHt)x1iMYF;+Mj@#42KfG6p%$v0tXf z=F)koKfGkPHHbIEIu)y!@I`Dx%<`~hkp9-tr?uWqFfo@}OVQ+#Mh&w>&y8uhAWFcl z@P1IbJFY$1Ey&m-zZt>0rna%<`Pxw2U9#)-9Ft*9ezbJx_4H?Db$+^%yZX>{Bi!Dm z2AM;L&nU;EQ1J(ZNpBvl6PxzlL6i)@Oc!Xdi@d1{EO3}ZZhiWYdooB@RQ(w%6OwXK zY3kHxn?}8Sfab_Y|EfLoNDoX2D)vlv!t|2CDM6ED3lRvgADA zg&MWsRwxSo>i9`0PGd|;9oMq80`#9jtP;Uh%F#sH5rG3CS*3~yvp@}t81pckQj*O0 zba(l!4>i3cYB#v9K5ca6QtYJ*MH_tA<`{5)niI>3u59~6_o``#v z>PkP-$8iFiNd}j9SRsQ`af~^CsSlzy(z7b5O!Vg0&?D&!uy>O@AQL(ZbTO7!m8MB&PV;Rcv*1Mt4DB$-9P}VNXR{&qlB6_FS zx>z=F@3~;}Pd&-nh7(mbpZ+Kj8BFI~Lu)Ay2eInZpKy3TFH}I-=U2)Js7d{P!%|xP z3Mab=1aUTi$9CM49ZZFUtZK@;kLl0#!P$bi-(wn~3VYR2>=Ab@O=;OIpitj~aSyd1 zDfd@QYmubEzk?82EQU$pG1;?yu0ZN@SAwAwXxu%LG)e%#3Bp#){l%6oA=?7)#%m=U z7wm9IuFrH<^QKfMlQ}o&E*1y1Jw+;O7c76#qxi>C12LaW>IUo;g=Kq*8+!125B4Hwm9bX*THS%F|eCD z?Vq}sA~3m7q+PxeuFf^K8AaURSWcYeQl`R<`f6>5@MgKtAD2wbWErymS{-o%UY67% z%^6XsjpFAR7(U;YiZ`#I*B$(&3i?Iry52LMbUvO>(|Q`&P~i*lgICD04ZbMgVQf!^d|{%W_b0 zsRKbH_!6dDcXczZloC1JBEZ*)xf9^doL!G?1wW#g-lxy=VU3<9@WbVp9#~IJ>{Q38 zQejXrb)X*JRB=g`>LKYm=f<&XjMXK#+k6lDr?6;5MYuLz>u&`u(CFs?P&>MdT4E;z z+25ZVCG#KI)E`Sx0nNGKI$n~Z{jTt-=Pres_EojT-Fw}Se_x`8eKqSnQHy$dXSuFl z^`9+W)5qL;SJxjV>-|-Fe%LDEP0#14+^ZPo!>}gbd-7jDXf2F>gBXI*G#d;8o{XHOc`s|wU81CHEFn_YP z7P7e#q}5%W@iF&}df;G*!uR85!oTXC@glwIhI-TIYIS3IJfx1kI0~YoVoVx5o~zN| zWoMtwpoH?1rwlQJ$2c-K-;I!fHGT=ek z6D_x=8{;_oJzBnL)54czroFWTLLLWBv3_C0CGD4c{F+xDGpDE5UITrYSF)RVX$+TX zd1>MMwH?IzmkP}nik0#GCmK8Gl(v^=pkCx51{#O%s6SC1N6wG1E)^A&5AoGw>8 z_?O}HIdKN2zGCvMuW7v*>ahZRzd98toH$)ZIJ!CSs0wfB(;u!p53L2E$gdP^n?#_&y=3^$0oAS3cuUhbr zzUzmaTBSqs^c82TZ{X)GiVCU0qW_Uj-;v5Q5z}cD^JJ6+7Ll!XZzlVpwblZs73;hH ziMDv{V7^VUn}hl_@eILgwmSN=pkiS}I0S>nM#kT^N}PL@qstPAbzsI#D(+W?C9=^y z==V7`2R8Xsrz944bXIRnt86fQNhvESm3nGEb|QUg1p$|LdRSlV5dN20wDw<@^M8hX zsPmq~1VMTBV5fc!N143>-=^_4Ull4sYlFf0g19Uq-9SteMT!V%gHk9R6~j8yJ*v%O zad%3bNG~<97lq3h@27ER7-dhru8oM_DB4|jcX&4^2D@`Rg`6yQ=eNKp`v5ey5T*8d zH1ihVabnO2z3$>CKOrHH7vmzZBlW5Fli%v31r#uAw7P8}XdGIb0VsW(Jw&ve5k+NR z2P}Ge&cx&|H%6~k+R#qe>#DC{D&9AZc8f>wyC)$M6a|T&njI-v&P9=LNF~>KixzaV zHFf-cPpO)due55fcGxbCA^`8nSj53}Ulk6+-vXR(XaC+KL_uKlNR|0Y#yFi*KM|w3 z=O{3Pah1>)gcA3*T_pR6eJqZ-IU+uw)_q|zthUa~`;OEKpag@1(lz!iM z(yLf$#3fRIf&;)DupZroJAm=cZMY$}I|2i-i>@Dl7`kAFMToVF5TmhG1J$PW7Ye5$ zzkn3dn!h#9q+K`V`nQ%k%#%14f+yg~PTI)O1JBf45dgQG+5Tn_fFZhzw53RYnz#G9 zT7GVAkGB~vx^z-;^ntcRJ$=h7(4Bl@cXMb0oWQU=gg)Udg|MCc@^3@Z95>3#cfx^MdRJR%qRlIvxG#Qi1y zcl!sJ-!!VpO}XetxSmcvA44S>ziX5n1$W!bPEN-4^>+N=>s z+oL#6&p_rZ+f~~@bSKtXa@fe(U3BjlqgXmZJG+4Q5Ooj^V0r4W$%)UZmkn#CBY*vI z#%>n15U7jB>NCUgOOD{-l3ljS_Xiw|f3|krkDR>U_J2>ae$s&5v>O-(-!bWVq_Tsw zFjCn=$KRp9LWKQu%_76VXP_$D#%oe3aQ8?8xSPH3DP!8Jn}F|HV?D-Fl`*fyt?!I+y6FIaI?mKm@$d}&+wq1J+^`E$hG4o&L`3{1FyG{9`+EcdM_7KMhgJ z0kVc!kM?PHvN6-;Jal*F19mPrjg^oXSg0>sjS%DEIFM^ph=I%LCPm}FEZGuX?)NXa@_9=(cqF1d{S$1`9d zzSkW}1-4avX~1gbu-@0GT;h8vEr7$sG{ORtwQO~3yIq(!&{WKl&q?1-f`qg1Cfl>k z&U*51y~Ma?IioGEKQ-+I7mv+u?ovlq3*PZ;3VkKY2ZcWOLFV;Ags_^`bUD$~mc~3n zTvFSO1@v!02@RhxW2RAt)IMb?@YiE;UbOFJ3z-{7GzwsE@1i)4R~p?1vZ`15S}%fN za^AAS(6Jm+m_?K(7P6xMf_w6SCP)V2yVYWif9G!{2gq6F8pmlv`j~9lLLoOSRGDHL zok@+c!xJuP%}K?XXHX)pfvoLXo^B!q**`4?8j0M3%U5$ROX^2=gi$M?3G|jqfu=;v zOVM@cr5&~B?FaP$dl~u7vBDw-=O}_xS;9@31hZfg2Nf9*aERkUM@@ISVg`Q#?zhB? z=hbsUak6Oj--cew0dC}BKxEw=2ccJZ1JLLsk@*j?NDVmF(?U|0{6~i|81ZkWEh$81 z{i*V{uU))END-HQa5N9B7AtU4O0{28Z?6thUgeH4QZzsgrH$4R@0IrVC*DZbYDn)a;LH)C@<| zWX`tMzpHX&C5dt14r^}`Rw3?FKLB&j@$?-f(P?w=qk=dE*!z(4lYLDm*{;WP&St&B zd32a$8>wQ|O*Ge>J+*Hr4NOA8AENk6U(zV~EAuJ$s5U?kt*bGIKu%z2aiM#v+>FGu zMtcKq(XG}^&->A8U*EK=6uUvRp3|&F*fy{I{9zhzF>OGZN)yFF-weRqtUgjTukN~s&t{-bU zKX0O9I!9kwSx_?T>#_`(Lam%+z#jX|sk)(pm1Q~}9C%4{Q^3?6mFyB&(30f!MXc3r(_`h8-8+2(>dUv2&Z^L5h^p$2iqETzE;=e_k2 z#Dj11Bs0(1U{}gjJ8Q_eD}{ zRz(@X-7XT)M3m|Z99Hw~D+eZO*lM@WXps+AMjUh#K78WfJ*l^=$mVc3RditbE{)*g zq(@`ue0~qsGtk0!s?#q#e;*vCu_}aa3YmFY>Ak6p8?69dxq<}`fetw24Al0ICm68>Ue)EvyE^rmAG=9jV=F>RW@|zkX3j@ zFpw06gq3gqkLhsvKh8>ykC?&`!6_(0-4GH@GY93D+Z6oG=50?bQZBFG+$8YpW9Jqb z=tX#`6b{_fzo7p(3V(kb`S9HL;g_ncQ8B>}MAZ;;Bs(VM1sJR7h(;J;COhFvS)p*K zxSK$~u-ZVs;Dh<{&-ic4Khn}g@(m-QBB-+_YEJ5g(62>SI66AlmNpGSOmGRlb+xYz zrQkI)K6hRZ_b!?2+OJu2xj!#4eX?V9p{sQ}t1eaI0a%34@V@Wt!EcUeqtd!@FFsdy ztEF9x428ah&}hgJBy=blWNZvT5{(W-zz8L zFm1iB**ZH0C#l!331kz#e?60c9ve=*)JC(|t=zxg{NW*tUpd$~OPYpiYekluQqVWQ zkOnu1duw_Kl5f9sH;ScGwez*Z+psm+i=hgo^CPqN7@@hdFNOWz-4CrT{Rv!ak9LG+ zp6k*e+8WCsTD#iA@8vZsKQgT?MKCXO^UBNMt5HRb1(U?}sI~?KmGRr$v6rr(k0;Ru zsnp}0)?(^7I0{=|ZPRJN`>Oz^auQqOS$1R4<#W0!b&T}ob1GZ!D()a>uVN(GvHV0o z8{MtNMeaQhFA7_{dSL)i=Q2o!w)eMD(#J^2Z}TE#D>62T*S=-1d2xrb!a#6CnGj3y62to&8yhVH&sHxdJwd~GGfL1rN>DJ zon)Xr0$I5ZJimEu7=fC4LyFDi$N%WLD&VyS(!+hS?SPq%^A60g(lgwT9zv^52@%+= zV@>jOn7P%uo}OWMD5Mh?fqj?ROOQ4(Q9*4VMy*FTn4LiS(huVQw}bT5_8x?tfO3JF zG6$CsSE`SA+qiA*?Ce_3FOK)cKxZ^gEN<4>e3}?p_fhv7HiNvd88*FN*_B2Vy>F3g zOk;@#?vH?e8lPjMyZ=LGD{HkemppfsnzRvwx_kooPCmvfJ=+r zW;mB$6ub8;^`>q75n?uk8M%whS}d4_&O1&kV^uq%me+kQjt@_Rsd_C+XE04H1TK@x z@zkFXv!iKbPJjC<1%>}y zYK;zmj3kU=-K4=b(-7)y_@tqrK z^#V73hxu4`{(q@XqW>S&sh$SzTXp(}6l!}ecH}kh@n6+x!})(yr*&`I_LUevDrxcg zz>70I!PrCAohptboFC@3-XyDCA~)lj%s;%L=|+>pvI{09`N%2JAZo1WxnUAA?9??Y zY?GsGSpmNV-Nny30;jruoyl$hAi{p_;W`^XRVb}2X6lXi)E1>ew9MiP_VdJz)HTZP zA5^A>K3&QFV{AAgtBK?MA$Zy|Bm?^Ijh}LW<=E>4nyfQ;F=tMhyn%Vs9cp|K#8AA( zfy9=^{#ckS{uwK?(e_lwnb}BNmphbwwVfogY7>#R!smC3>_U}JB51qo%jDtcym3JD z&g$~kJeHR~%-VfzD4$c7iviNKd2VD&?9zd%dD0NX7IwkuK7(IQJocE%g``~vfnQ-DZnLv^T(~$g>r6gx1Er%(2L$yc?{59W ziqFY|UB`914l5C7 zkt@J0XKg_!`C(NwIM8ZZwVx!*8fMq#SFtvB!+zD%itY9aN+2R$k5*R*sXsj;H6v!a zCswA!aA|3EFP0Aw)FLNUG$PSBVALilDNC2V4I(+hVcl4FF&7*sUC#u_&3!_Dl6!ug z&kt;a#5qUHZo1WSt_><&bq^ZDO7(&K)$tsW}w~R1ro4a<6TG0H_QgeC-cT?yLWQYIB!~5KMC@$62L)1 z;jh#&ivb_;P4G3d{JrDtLp-4wpl0<=H=zj&r)2fB5^xYKr@7f!+yMty+;ox)Cu}NY z@;cGMITdILgk^M9gRxuVp}B&e)__2&ViY7Aq~ek(;oS9B13FBP2VTHdi&o-(nOsbF zgcrJJEoMPaY@FbbiHyuw$^TMP*RaPya~`-OBhbeCjjN6K11n5!YH0W5;(>%Tp7Ug* z%J9P3;u{>+$hAB^xj@&gz&8_A!r}YQ`>NCzt@>Fhtg${`wclJ$!`kw=*!8)4k`CK= zRbI3n1ykX-(AsOCP>YMI#``X_Lz7(Y;!vg1=s_#oO*6HsFjlE@^K+|)7?}YeZzw{) z8;H&u>aC?$dbJx<2xne^Ib$p1-q1leVIGTcN0V&IO?yLm`IcTmfsY>kFk(2bX-Ar?I5ev*W zwT=cPl~9t==vL9bjt?dv()$w)Jd#J)Fyh!)%2rr#;id8l0vca<8J9w?)a;9$lNL6C zx;jqj9g@pEoc8#r)LfW2d)9BTiza!)7*bsL0Yg!y~S21lVPa8k)UT0v0 zLzH9pcZRdAIjT5E?2?-KxpWxpS9Jh!GUUWu?#h&1J7R|S>p}s)legEw1lN7@r9H8ZHKl@^>-uGbye;PxzQLrJPq zWar)>1hK`$keU)s*?{hGSiL+)aE+s%SR4jQJ6b3cbFtr&Ce{>3PDL>!TpF063v1i~DtX1vO z0{l?LU!A`_KiU9X8TuM0KO%x`7gZD4onjrS^P;U1PcN`mj~6DJ*E79jl_mg-nbA_s zwe#%~)cFuC|DH9SfCp*_q7bF~Dq&xv_8rj?m=t6`r1kp!6B-5A0a|&#=`(3ppS$sn zUZRW5Ep7)P1ryq+XuDTaNrs=v}MU6^V4K(W^Hzy6L|W zp8QGjlew<~5PNmHpc__TI%dPA_&cp$>hv6GSuh*HVC>UV%FoX^{(%>2XiM(xwY9ac z^{?%xujc2L&+P4`Twr}eQ&ZEzN?C76L)Pz7afo^?xQ>G`1U&GI@f}HM*VOV$Xwt39h!$b?M_hBH>h+yBo%Yl+b@i{|hRAc(1&c4(hWhiXC&J%Ksn6$PI$Nf2 zD)nv_RTY?yYHq78^5&nx86xtvf5fjLrrx}{V54yb4AQm%+WN}WK$K0HQkLkzG&>EZ{E5=#k&RHMmm>N{zonE`HMK64 zjtT0Ad3eb@V;x9A&-Us$zQ*JQD*VT}3QUo!fl(!Bhci-{O=%np>oX3rt)iF11O~`( zX^A;s{9Z}eZb?yu4}EzkZ((zYvIiB8@OyWv9-G18K=3;%NE4dwt2;vSv9jMogo%L% zPT_1yqB7KO6-8MU5X7zNbLOVXHc6z~==d2@!LpI!cypXdM2U4VRKLOzwb(f^MbubA zl=C6+0T2ov(WMp>B)XbAU-*$EKNyi_q-@pRRL+th<|wSz>HmF=lcL;s0hriT%7X81ZV0L*5dO z{+%TGi4lnSkmk~(48cZa0RnXtKAF#OuodXSM z!J%YQo6dC5xtvC@zpJHTr&V<>{e6L5Z$*=dM;==3M+C|&IO%`HqW^j&6XqNF8j&0d zl&L8$8L#_a#kao;*S&2VH)a;Yd)?HKEO+=1asxb zGV*O|9}j?Y1A)I;X5v$``@^HtY8m9A*iBsFrn3||01}WVxDGs zmZoi<)qN0&*WZtHrENY-f zSLFSgU6m}GT6$?Z43w35DGz&7r0Zxuv+*u={f)PwhWi^V0_T*5uxt)7U?wiHq+sR0 zquJ;f^TB}!2q$A`7SH!0H`H!i;{QypTFVj?0Dmgp!aO12crW!b-aQ^q!N4CNv?Eme2?|3f!-Y;yKL(Do8mtlr*#`S+MpW~iN zY#ULykObcKtq65IMaoUQrX8sA{lua)=qa3Fsg!>6@5RD0{z4m@!Y|I|<4D&_#LME- z{9FqBMk<5>n_3on84t3uh&oz=*ws}Qbwogq&(zDHZ9h&tpU3mlb&u& za|`gU{_XU+X9(HPgXN9CK+li!r%w?uG!@v4^>#O%%FXSEqW7`h{JFE8m$*iMbJPP( zP*yisgvMiX$dQkdu_LizWT6j6d@a^4+Ky5l7V|F~-oqobljr>SP=kq0Ut(rKcP=i+ zOk#$)@ECjk9{$uL^npj%Gl!U0?(+7(3rb#3uZ71Nx}*5@4-d`NAM|^n7+ORJl`g2g z&c}Y7-cNfw-<5aMPu$QBwEOug!InpFB1RHF-K zj=k!!feF3Vlzo3#*QuTR4pJG}=&3XXBz|;JlsS<7WU5JHGuFjss`0i=SCg$=P~ks= zRQeKl!&8sfxEzRmOR5eWelS(_Z;4`ro8)rJ(i^&u44j)h=gk3mEeMiS$y0s9+Q4doAy(E>#7HU3WiGGjpM;XUa-!tl>U0tPU3;61CcB-|gz&4G9`4xt5uezUf^W{=c1Ce#EJ#v^Sv31BI(~$<2z zJBv9Ri%+R5FmAK_HC$$V@qBizeL=+B?%U&5_vW>!eeqn=zs0BN4jOW+ebJ1CioDjp zt>S(EP1Bw2xT}A+DQ}QM_kOMY1GrBkt;{fHX}Y8Pew}EB@P56!Fe3aJ`yDy`_jNAR z<1*Dtojatk1mLNx?7PI$6f^kl$j8Xqy!(XvY(Zj6--nq~n}c6p%$^n^gFD$Dr17G} z4A&ypDK$gAgWOK<^ z=#9B2nUnxL8)!}ba!L9GD>X0l&sQc>eTKoQcup%oxfWL1$H%q@Xm%=DNbaJUh}z$I zf)3PYHm)cQz%01!4HVB4cxvHS+Ohz9CvKr6!;A3$n;Z2mL%a%ob^TEI6!3+eos(r_ zJ>e~8+B-jN2a-y}r;ng@(f$mSOR1c9w_RetXg}qO(1SX~{oQ|wfk3g_N5+#D43&7J zk4o^nKSwe!^#BVRP7C@h` z2m5b=LEP)*X`AcKW)#F4i^ac$7OwU)bOu5@GlM5i2A81*$=Ob92j~3ayVdz~M2 zVukmi&_)XXA@sQbg)T+CJkGcylhjxehH|=aFpZE^#Iv%FfbO*9^m9^iFM$Jfn#$uJ zq6el&yDCq7bi|rZ{db78z8~w4`W*z;E4gWxZ>UU&PJLgh*kQO_R+&Py`Hd+k0Y0i| zEs_!=XlsxpzbwgeB@gEjWD-2b9a9hP3aXQL%1gmbD2(TrEINjq3}p-uB3mwYHwJSw ze;8SOrO;RYZM`a2S&Yf7bXj^oGPan!$>3_DeBznm6R^1v&vVA zG0q}eZ|S(ROJ9DxrXgRC)N!^_Y@^$$$u4HaG<%@w^Jxl;ot7QaQr+~$nVPJ5Lr&UI zG04YFPJ!vEnw5a}0`4|U43F(a>Grmn{?btR-urm`hwo=Bgm25@$X;jX>S)o`Okw;8 zOev*_p(EYFEX%M*6e6{U%^v zOl)38%WPn|#wmopO9U}O;P5%#CcC+=rM7|r6_LaGp|A>4~yZ>*I4{{bl zoJ`CCCK7TC8p~L!U|QjbELW&4-n`kg>aLf`*MPOc$~%#$lk`!i?`R9-Q7x*Iq+3wj zw1+EwIwr&02TsdV(jKl0g=`Z1Rel5)y`I}(WHKYMQ|O2TP5k2jmi8JAV9I!vUOg-S z4_iiD?~gD0|HGE8oKHqtjyRvQY$iPGoZqBRpo}Y9_cc^?712L|>`O{;W^qop*Fu>& ztJJN~-fS_poEkCv{dl%yBNQ$}nx)f>+O1Yb{nvm_nJx)1s^Vo{%2;tGxJLeat>jGr zar`vN6L_!lXEOQALp0Xt-shKozprX`J5_O65pGyP0UC(M%nU4maY9z^soVa+h2%xX z|5ExPrz5HkPdf_QHLkl?N%irN*Jkso-=}1HpOm28u72rbNTt4~dELEaqRF=8jw`B{ z&=9P8q9Ytufo29~Mja<}GbQ6l$*ygs0{~*PV$!jIpyW^4w;00Y(xdA>R(+MLY4Uk^ zIydyF92?&LL1b(zhygn2_=c;{OecR#e`vyB2W3>159%1~+J-JvJ>Y)bNIF>noK_LX zCu(4oByPY*m!4*$O!+Z1pQnz4E*)ej*(6xULYbm@>7-24l)j#Ppoi75h8)C?9mDp! z(dPLe#U4EUWXduXPqJ#DIN-;SlJxC?M2(Z%pHea2-DHxca~<`57IVg65wSf>BEgcv zY8Cm0ahZ9KJy`%U=AdNVFVA`}lmmD@ql}k+wrHM}Ed7)QYA;ddJ4#Z&`)?hHsr|g zNLkK_wGezRZLeM&hHJ7t$mtT1Q;|j!L6Y+9#t=+q^aU9CO`+J6z4R)Wv6?|7X0B`$ zT}uC-nb48|npg>V>vnI7x~z|a!H8a%?s-v#K$hhtU{`g;1ciPlJ(9~JJwu0<&JW^g+y_}FE63W;T&n#uS-QBmPx zU_yf-goOePB_tTg$i8Pr+5S6lti{FYOGx+=I17RGumrXnWTQ8VY6xI0CPSx4RVwp(;W*7a&`!Q4HU`BJl^p9+ zm%FpVK?&Zsa;USv!SIY=_+!cv5${V&p%CbFN?7ChOZ_pP6_dkl{O`(`M&4Tj-4*LM0~EoAV?$`td&b(7K@e<OMp-*7j+_|Ikv88I zj0P%3gTIY9~C_NQ!CWo2{V$B;oW$?z1FXOy~JW1BIA{>&PC^!IA3!2-~)i+C~ zDd-qU-)rB4?Fj>faFU(hkJf_**pUT^mT-+`vp9thF7<&ytU}Dgk-fiFw=2}nKj5`9 ztqr;62`=|+fETngZEKpH@sXf;Kc0?o06DxeSl1L+;sqd9@8(LN;3bwRtdz3)n--1s zV309jS$?ngLho=-1hD|9EBnBVUj#CdC==q2CR?PefT1W463H{V{To9Y563k}#`@f3 zHuvek4c)FhU_5JHDtK@frl;T6k9G!K{HH%WPKfz@U$6BW`FD0R zsfQtM-FvH{$L9bDBJ~g=;_xvM@%$!J+INe|b>@A0>gy1v)MrtjwfX1%(5BnA*RQ*~ zN=Nc&){^7&rb+PLh!nw8^7}7d%9E_x`5<wWkrrp&X~+rEkPqoHkc96J zh)NNo|6qQxP@tK%QCbvB604+_cjTuw^Q`|%)*t5iB(vRmo;s~|hRhUw*6AnuXc|%G zYd_-(sKl0nU^>IOw>3MQp3=L+uQUwhELk~t*C#%yd=yl|RMdHzv!){0Y+?wuP&_&? z%NV{m64bwSdy29Qr~YhRx>j@#ENtd`71?1chKYzrjy%@rGm(dk{L9479!El|2$H`4 zA=P;uDOwedz(tEJ%fZYHL6^Ll7rbT1fC3Cy7euX@=#L97%FFMBdUA5A!G-rS^G^Sszt=f@7|M*-L2B%#)wt|MLWP6H zKyP1N{)${>_gVbl51JBRygwn$YAl0C@O7NR*3-zD%>Tz!^5Xfg(_}`-&t`V>*x90b z4{Y@2x8RzBIp+KYS6>bFAK3W5gkBeq?UVl-Heh`5`@X!Yf8~bhX?*3npg(x|yL$QM zjO$K|(%(28f?M)idITvdn9Y0ED*k-LzSiTD_J6+Z}_&R3;xPII%6 zjri*F$S2P1Et8}&^smS8l9XJPHpJ3>CpyrPNPLpy+;)y;_UkdR3+sKD%7t)|Sdytc zx>k;waKUH9e7H6|@!-lweyPYgOMe=iM?ns^rX^wBVs+7Xj#0}-VzSHfGUibNe1tQ- zGT6@S$cov6JA!hdd3vqMBAm8VktlP6vJb8fOcj#=|3_LLaU?M^N5_CT9v`~#Y}(QH zU!hZiC3J@5P&Kv+zBQQ9AuAaRtJMsIPa1ZQ@mP)M*Mj8dGO7qxWJqI zGMIbhr&Cikb^$VNm_rj{4tM9KEG0}&v+8cPJHrOJszZ$ivVX;uv^xkrryVLeT4ne= zsy1`VV7;1teE!52ZXr^@`bD}KD*ly zj1)^^J9qS@G%1&k%qwNL_Nr%S>sXF zm|fDW;R2cx8mx|#IJrPbpTuDs?L-)Gku%@GXF1$IMlJnf`s{;)ZjjUNeYBUDz^ z$cl`}SjNsP!2RvP31<1Z&nRQQTV;bLg|@Yty(FNATkLB`eZ1b8?`+xJQCQow`g$Tb zI+r-szFlkKPM%JB4?{vl00=&pf`nANPRQO%fz$qUqm-5Og z25QpNItxQL?@nl$xa3|-2v7Kr{eY=6mtX;1NLC^JmIUBlZRl1WcR^#;qH|dM2TzWI z<&5S9a~RBZSjKDRt_y>)bj1h?R-zvGri$}>%k`F`-s0o*D5XCoSZJW6YvR0Yjbi;X zOl7@_e{jv#fW5*xJAcJhivqrbH(l3Px4CGb0h~e~(w-izd{b8#AYP^A2HoOUCAhl* zTG0g7t%i7MOV4Zm-Y(pIxs1|n)DkC8u<~9(q=k&q8f)*+@hFzOY7?T5*WnV9v~Huc zd+4fVxc*^qye0X==JIU!#I>Vj*16*`tZdx|M&f;7<+9nC(rYxij;;t%E0=v^v!~mC z_W501@2B7eZ0s`~Bc(E*esnb%h+9MKCgMW3NodG26wM-Rw;t(ach^%b?-hJc%X*3D zAz$mPPsd}(?Yhm>IVVHswr%cN%C?gwNXMp2#`g!I6BcE+b9%N(#idC}NSm5R5Eyzc z*ov|^uiX*`Z)Pn$6>Tb*`Qus*NznVi@8ukHUZw|T3~ucx)( zyY4y@x;(e}WHIyz+IzGaIE+#rvMe*s_)lz{dbF$6U@uASa3&cC#+>JyzGM@7f9%8V z{1>R3$X>%1$y_gok2v6F@;8tYpwjEmZ_hJ7=JqkyRlj=MWv9!#m%Lxbaimy6Tb+q5 zh#kqv#k-Fz&RNzuV4PupNPh%Ub9cqd3)iPO^4#dpLd;0o+05nx^~dAUMdz@2v&vtB zGh=6v@P$gNl#SPfk<-YdZWo`u%QuM^D{@bTChFfOwL++_Lo&_4&n8T4cjU9xXB#9k zI;npM?8U@~#kpvBQ$&)GQTdQvoxeFr#&?ov%FR*Wd>WNrTWK!H_~qxg$%haoYFKXl zRp>&5ma45WBbX*%pQEtmpXwwh(0{Seq~X0~uU`?$yg#h)4c38-;4g>M&H& zW1NU@0XS*Fhr8h|bmh6~i!98S778C}mW^;XJ0BbP^)faI@-&Gkdo-butWEUcMw>88 z%U#Tj&PO{iO9>DrCYz(gEOpT2)os~2s^Uv%A*7-&&!<)~XU(`PwA{bgIzn<|1UL(~ z2~g}EQ#qTYv;L9SWE7`ESd$_vbbcg*O6rkhjT*BXlrcM710KKLCwDa=&|(Lbi#)usj{tF)COxUcnQ` z9-xM1+5ur#h_7>+B$Shgr7?#ew7d%wBt~(on4X&%O$6D^7FFIR1?e%1gRB8ZC?!*n zEtKcRoZuA3fgur{&q+*iL6QZ+6TzAa<`&)Lv>^4_gp4N7Ma^ZJazv*o6PE^X%je{p z~>KFysJ8da&1@zbrU=n>(;;j{{pWtUPd$Xkq}J8T4aVO3Ff!~W(x*O>BtWc z4(9#$YA`(uB*`Hb)a4LZnOAEzWcgFUz)3*cO|`XZeV+TYZma;OREZxBf&rA8a`g1+ zlU;ZfK_bQ*8YbZMf?i&=9I^}bUwBKxw$Zm!xz>8AN>>0I5H6_}tO_EpG9*Hun$&3e zj?)+|SefhP_YHE4Q>j`R6EUY4v2=yIGjvM|8eMgI)-Syg&|@9R6xnn0m7&c1%0y z5*O_RMyNYoF-h_&2~I6l%SAl!c0?YuA=e}kP}rNZ-)NHHJ(f@RNjiFeAyA6%|4hdF z-(Oyy?JMWg{R>izhFY-`D)ci5W@Z^~j$lx87F6*B@bo z`mJIFTg7RjGA(FgV=lSPXx^_~)J5ZBXsXBW-#8?^ns_=*?k919Lv)gsw} zw23Gom=|El*?uGb99_~m>D}9NF@bY2zEY=Aj$)GKB*J3iqT?zrjkpfSIJ&yQxj?yj zh6^eK_t$qpyvh6Y=-K0_=Z#t3ZJQWXKx0S4ZZmDBdK8?PRKm`26OttJEs#vX*>O05QYt{hKR%&k@=D%cmK#`>+}N ze=3r;nxb1B7LmL-C!8hMJ~y1O<8f^Dy4o;I!N|^?*Q)K)$ZurM;JZ z=-&iDELe7`2E$9FRgefcs z!lmLDJ}Y2$OF119P7itFB+5zQPEn2=N-y=YaY*b7wehT2_5{jyphSxn`=$yfcGZ9F zC=AbhO1&_AyAU1t>8YZS@Yb%I!GS3RF*r&nN?D;KYmn({WK>Q^*%JMf^g)s{nES^+ zfUqB*I5GQ7KzTQ$%=^$i+z464ggX0Ze?KynM690#9s9!DQBQd^RsSiZ{#OxYYn1@& zuK)Gq@xhCy_4;3r4<0{%Hq`&RkM@bw{{j@;LeAZbAOiV+bC1)+L zBFa8iIk3y)v(SoQ;?{BiXc6ofZTurdc)%kvS1&vEZ74m z4ne(1 z4c8sE4Al}gSQFunC|Iv8LATB3ocj1Q?2f_|hjdy)IxSZl(rLBZkWNdrA*YJ7ee9en zaD|_#!Ur2oa%Oj21J~L4yZ`_Hu8J*u%=mM`Hkx>I0L4hp%kPgYTg@xEwpK9YD+Om2 z7C}=qDT^{EmO`EHqDSNH97XX`#A&){-zf0{$Bj=gM>ix%`q-}-TwRk=FfInvilyrJ1=a1ekP053r zw|ZGp2t|?jUs=}{KhL1KK3(R+jG5~S-yNarC;mr1M(&Rgg(tb4 z1TRRAo}&HAndctD^J;iDMCT3BdF}S;Mdt~m7O)&*@fvN2#T#PrKE+r(bD*tQJm<(8 zja}KCb40)Yz#T-6Y|@Yz?g;VGLeMhIc`?gbIrX?=wTeopw_Dkl;B09o7%nxf$MDHH zq35el6dl6ooYNb^=mt_`+Jh%WpF;@Q&_3Z768~4mcNzb8`1tWtNBrN@A^z_^+9wkK z2Pn9S_`mljUGabKPrBp(#55!!bjJUc@kfjQdw+5!jbMGzf8xa<`mfRR5dAkq|9#5Q zfA3EMqW^I7lOg`kk0e9<-yb{vPh68#{Ga&OhT{J=7vv!(bq7HnqFYyxhc(;~h4v-%7!;j_My; zr~d!(lf$RapV#&Ohc6DF9Sr*a`)ChX|8Jwo>`nvtO*9wGJxH>v5;^Wiu? z%gKZ~VsiS$uOt1jrK!bnj57ISMUMKlwjgj>A4nvy3cJGkbkWo?nTQ>qu>1z+5b;Ny zve=SD)~268P;nf~wY1F$-Kx{f1Jf|LB8bKZ7*bukJXf)k0={bbaKOqu7bgs0Kujz{)#3T9)>X>?S&N)`9-(<}Y4h5yhY!s5qu z@INQA1S?l{#KqQ|AZ@L$x=PtiDg=CY(Dq%k7_d~a(q&JFEUx?=C5cCv4?7~?SKU+w z6Vesz{G{6Q|1uufQBJ7U{3MX(@m z72DHE?E`Wf5u#aM)xGF>7g0DBLYArRk!7utg`F+&Xe!7EMo$l)pl!7v^g1nQu|SuM zp*J|6k{v;gGnV=dKj!=&pzW0D#d{VsVfh>vpmcx!wBwBVmGZEcaz>lm%tn8myQ$%! zc2vus+9bHdma2YM+EclOdtghhZ|Adv1GIgrwGvWnMz8an<<(yHW%;=8Jtgyvp3io7 zwH+;HIpKnsTAL*|@@7V(S?$2-C?QFYWY(dW@+%ENNRpF^6|mf!#nyoH@ZjmcKYQ^U zZ6E$f?-rVyJRy$$R)D9%yiX9I;+|Z*zRhyN{mIWl*|=I)jexZxUx-^grcwpu7okDN zmar*}aDpN>AFC8T0(g@$jbvaju)gA6#0ASaI(mD8w*UO+9k>SGoulpFfA`X;4PM`{ zEMk_tQN}8oIm6=n_u_a~MeD1iQ+BQml|%wwntPO6vNd@slH#$-h$8nE<)q}`*fVI?-gN!6?$cO3IwuoFHX=69827sAnH_TDaCqv5 z`q*%^8lD$F))XY25rrYHjeU-dE4ei`EMC4AlBGV#Qzpox@echNm)MiQHT5}qbh>%0 zl@6kd!qGe`Y4Iv2_{yb0s6qI+_Oeah|NbD(uAi3;l5nzW#PT1We;J0aX`E7z2PlT@qY z=XH(GuJTKt{zkK1G|pL(&|SoG^cyc?)90q+-9lvuHT<$-gz`6koklEnYROwWbyEML zk=RQ0hlqYmCmlk>hodMWSuxU`ebD9+rF*NJw*{<~;lRg4U~y972bDUb9z1^Px$(Q>FxI%v4Pvc!qnpp`DnP(2 zi0NAAVkfs$`Wh-_pz1x(NhDbR8NQ}0M`c>j1mOZD1oNVHZL!p(4vS_uoyx#=tdqSf zZ`WnTc3c+hgcKwSv9o&-?#T3F-MLn#_Jqi_e6SCyzEO7(o#@g7((6)ijHE#0g>qs7 zgR(EKZwzh>Y>o>WK{y}rOBP@K2Nuf(DsPz5HcLS#jRB}8e#-~|MW)(k+juRErDdEMc z4rwRmYwJA8F8>%VX>zTDPuhB*8=TS}rc+wY_pF%+wZqNKvxHFRv!btf#~<9Q3?xJn zAJ1@(qk`mp+Kt!E_p8h~Tq)R8mxAq3@35n+ST5_rO zr)zELKU7C(E_%9;RbP-fjaU+7Q#*2fx7Pl0)62cAbp_%ZKzMto(p}Qrt_w8JS?{gs zPHiW?JQGrxr9XfeY1!$zZrNp?3jSSnnTdgDiV{jn&6t=JEjWspVxO3u3NOd$RQeTt ze|dR^E-JInN0E>KEMg%`8wcP^<m&A&ho9Qc+Sj=C}pO+V-?{q3X@G#e5vw0*evoD>x%R~q*?SI z8^SwsY=-sf|CR;R`tj4vbrEh~>8hg@1Bl{$YkI9S2J&`A(0?m;>Hna6%&CgvD>?n; zm}ERwhaj2?!t<7kRnKG&L?nF1sKRW(=L)920pH#5q~FJgh;RnyVWqO2AMho7P{Q9S z<%5p6cjUjZu&kB2AK(n%(2u9Gf>fr*7g{vdn)t&DR|F^d3Sy!H`5)iI8xR&%$WoP} zW2Imm{is>UKDwxmke8S;l>BkQB)2}2X+F!l2m<(Mx?n_{GS6~p!0Q#V)a4vc$ilCQerXsr_Z z058;71M)mOP7&)lzPf&n|DK zm-S*Z&TWCW$wh#)VSFnC%;iAB zRvDK;X{n$wDK@iOX*a1}BZKG0*9Th~i@IF>rgku&n#~%tl_`W1x|snZRQuaH#Jo5+4@{0u6D(SUjKI7Lp_;yp7?>U}87v{qs}lCz|e zs@kWi#rIvf9Q$SFsLHF}JeUW#>vRz+G76EZJzZea){BL7beVOJnA34p_m%C>BZg_R z;pAtU#_ZvXI10b3*0|0XMb`v9=-)TqQc(r}EyX}$g!7!Nvs@_P5{se@FWmjgmf?3X z%?SjN$wrC=gn8>(F{K!yD`ROPaj11=7k2{w{0;KljaY>lN_$32R12=Dk8vy?179Ca5-Lc<(pbO`<_riGBcje?py?wbmTgPm zM+J`RJJ(;Jlq_Liu08N;f>RaECO?PhdQD=AbCPTkUdZj9g57YMlLmvP&j1g)USo%& z0?}p4xXEvJL+%L|WD4m|i~Jrnh%MGRF$mUparOJZ`X*7vk=R75654#M+Mq)OL9`7@ z&}!R*ZMA2or)#&hHfSyim^ytPws7UtuD6;$QT>!Na0oZac6ZymUeyib5Cx|C-!kZ7 zW~Bn1pMQG{k1Ddi2kJi*I7O_MJaNl$D%^`u4>>lJ_C50%+lK-giYxN3y5+6NZp znd!pJ#W;;ilxGL3(_EK0RVD=!y9f}yKHVE4`kY309Wkn?pTMD{y5g#zgyQB9{+Mxd z9U?BVVl1HP@?r0xZ`z5?!6Pqe3Txj9;e1u}Qwo;5btLj}; z<@FXyt_Q?0HBD96u2j>^SwaM?Sg1oS$URA9t6#3!FEmN;OU9QcB%OSkNtF>NKa=^% z``g>;i4NjBnUQ>=g#JQFLH+*(R$Ol-f{%;{s`yiyI0l!w>H>w^R9=-H@Sm!Wlhj zF1e2dY`F>yR}1Mxp4!mG^hzA7MHmb8@Aw|ipa%Y42o2#x>ezO%^!itZXxRrSxjv^C z$2q-U(_9o4v60Ui<^EK`$oQ2DB8FmHrap3PDYVZ9rTKwh2xql2Q@QsFuWq=_Bg~iQ z^XBO}OBNMcxu6l_49VNK`7XOAUfZG7K1V< z2KRZ&aye1oQ!qT*Cquc^OBL8QcX?{AdWyMHicJ1*|MR54YciwxLFS2>F1x1&r--Bx-)4%uE=?7eMnI8A^d)%-JA_#$zN@^#m+Br) ze)lmr=Qtdl%Ws=`%IC^(fF?qx^_m{DN!REapljYtYV zsOJds8!1X2P9iGEF>tBD{)MdPB$lPBI+9ok4}tU?I_BZ9LqWSrcYu*2w8%4+xB|Na zL6r4KZ*}t2(cO$I6geCf;3raG;y4kT1h#^5lJ?>!uZj#}gu)_$YiL!lkLq*D(d*OG zZ@OX>ut4iglG9A((^%t)AQ|Q`*wy<23OIg!`pQ*^M#lpR7~Tgnw4ALVszGM~jUgZD z>(kT8>p%Xnvp9&gZX{tbkxxhGr{~A#XMZ?0g$%$}S6QadCa?bnRMY?c>VN+8c=ESS za_OBR*{XYPh_W|#FopL37WxNCAB8pbs*s(nahAzmERx)T3mPHC>b0}+2oPnvpVU#^I+FG}E2wxwLL3HYHB5s*+ErxKWbiCR@ zjSEk84JHP@v(f|E=`sh=56BYt$0>5r zD$Uxb8qQ&$(Dfc{*e|S2c4QRP%p!8`QFI{kbzm8>k&9%wo7wlEY5;;#N*FHpi}qVq z`IfTnsC}<713Oj%7N#Qpt)pBm;$w{il&cU3|B%yBLL>OMuH>_j{S8@?owCJS zdIrjDaB|3Ba)#D8k5=6yU_gJAl2=}S0f|#U{z+1P3GA2f@uEIYMf6c!dS1x?H|^7v zg~|<`(Png+()82Kb>N^Xo^Rw&kiP)0rwaI0E-BJ=#&q?aVzN3vJwu;T!wu@@@#;<# zq$KwPi$j=#-jz?=C`${|bODQGaTg+^|BURjC;iit{?6L4ro;NhE{}h4{h0dPULV=G~aTIq(c%$P;FA!W}Muk3MJzNK5)&{ z=Yr{ur`8v3vxx3>$Gm9DFr%^_s)ZrMmT_6v1^tYuf+RQs29;Z@hUMDhyktbC_p%=x z+$2_s)!wrS%zVHhu@)(wVl>eagPm;Aeb=U80?Lz+{p#)YtTvG9;e#d>fO5Gw`nq&@ z4472>5iSw_DO1LqK+Esf)$9nejYf;>wE`%vHv}JoD|B9kBSA>9&MaxZ+RR@@Y<;q1 z2~L+MOLj6(*!<+X^HY5O$J51kXYuK)H}m;-@vHdk+i3oW1%4I3iQkaZ*WbOGe|LH! zcyt0@BX*J}kd=JZ!y-c<7$94r1ow{_}D$TZZ)H5EvwYy2_I`F6@^nxll=lf>Av zkBdWcTO7K=1VMJ!n$dR8w8~4KML(1I5aaZp)Tj^~s?voeY+KX&GB!sV&Chvx~B2A8{{{1H@cHJV`n>f>CvTTTA6=&ITGCA%$0R=4WN@M@`CVMY zx$C?tm8k7M1>;i>BeL9ub5$}*X7Jn7u z9Zka0YyF^Kna$KIw&N_0l}qfelfTYplWCE* z*XqQm5M565?G&H~0d2grz1M)_sFbVbk%@Z7-9QKHrKcaQ4Y?s1<9RpS&EKT5gpkHf z)Z-KR{DB*KyUoZ)CQ`cSpue!St@4svDXZyrC(3l)HDNVXGu7pLTj)?qIF6OW=r6LU zU5@#ewgPJ%+8l=g{iIK`?4EE=V_nV{!JCFyo|5rhPf^$(AAED80oW2r-vT3-qX7AS;M&-trK)#~Kq_84M~cmjgF4lkeX zn6X_2w>faxx+W+kIae4 z>~gDqP3FSUErk&KR~{3zGE9U;KI^jb)CBkmc(#?VxQ&4x=4_t_K6L0mdD>jQ}I^8 zQrBU5TRYdUt7<_u9az(xE{$JylRn^79-PQx0Xe3$Z?bwoIYzVDwesai{bzd711;BB zTAD7q*1b<+1Lw4ysNmBa-w}ZzKZPdfl;h$UB8CAVslLQgl&}aVvY*sVpkK*2i#KXS zxgph83;6U@_mr~~{ta?j)2R9Z(EEt3vxMY2M-m((Xu1TEl$p6Y zkv&>uMdHJRV94=C1;S2NPH$*TbIROBW!m#^b&OT+>OFk;e#g+I0yaj`?a1?Zfy&ow z&#<8RY%@=2^k)*%_NjT+IO5gI5JUhI2bJ9{RY9Ql(?3tU206qG-Aj%6aCQ3u;XKFD zod#$v7+;dO=5-#_E{F@@gzw(5H?@*#y=rdltxL@eqr0|XX)V7-cqQx%2&hyk!xA=% z1eR?%Ec1dAxHfXh{GPFO)`8mkx@tingsQsv7ORKiR*f-E+PS~JzErv`x9jtF#A z0{5!n8uGNwzWN@01zv%#I;{UbRIW%&Uk^|*^XgDFs|1Ib^%6j-yih>RoVW8Bx+Uw3 zam=?nw6;~o&@;_rH9Hqq%7s1K%+-!4WLG`AowZ)G8sus5l;Q{LIx6TE-b0WX&N7l# zg3lE{$fRKFD{O*J3Z8(bPdP~#4hadK-OlvG)@Ek<-Xg)vY74rz0S9f@Oc7!21f;*6 z=~9|O^b@=+LJ)_w0Cv}!0_*g+I-BbBO`UEOzt73TZ{T)GIVM7yz5~D64f)5A*q*EM z^-6pXO(%uVQTyF?i1bP5n&wSTv?AnZPUNbQai)pkRA?Q)S5!37hI5kUb_%mygMetN zTFJ>jdgGJVY_r>W_5w<fVp5~Z4@*ko+gv8&JtIbDK4h} z`ko+e&N2Ez$)*so^^<-?2F+%Bq=mYg7Q|5H1bQ_ssh}*dl=-$G;LRC|Nly5hrlcAX z@6N}wvs5#fSrRJplNU5Ib?y9O7?EpCvR;XI-^%AvPLf1a0*k`c^d-8;vV-oLs~)FXpjsKb^T?fB#h&uJPloo7HT z>^q@~Dr;%ahB0CFhZv=+uGEgUK`~epl6tZ}?g%4t9cMD|(AcAZ1CBG(;`RD!Tr79{ zILVk@d3lDZZhxH%_&yizo<|nnwkOaH_t@K=$K2}pw>h3I&SADUL$)^M=EYHP@U~_z z?2Lh1nStAwVe;(W!USwmyc?7r*<4&3laNixunmcv%A9AL0hN=2?QR-n)pi_Uc&V~-!W_9^9C5OMfBW_mB@bEaaDIOJEjoI~_?*Tu z>Ec@rvz6T7G$_n}Rz6*4H77L_mK?!HhU&D-+}?P8zST~p%d59HvdyrlJUUjyxnKEt zMRO9cU#WRyDh`$EmZjTgGA`}o(mpQjkls`UIs6%iW7UK4L zb_tXCC9x&J^DisYW+9c8MSh^lZxqSfyPVE(9Gjk!BcH@5#yKukTd}d4&%u`~Br-sj z-0dCSb*+*g&l90OF2CFR_%6Tg*WRmi!L$IhFcjrvWFyXrNsejKrMwVlid7IP1X#1y zWPF>5iR#>m?A-}`-Q{8r@XiRa3C_cQ1rOI*z-;_L@+qg|0_3pIhK4m30jg`gW~tkt z*=qP@@scYXp7AA4=`UD?r+rGPFPV*0GQQRXW2u&d5?_dw*Pq%YVw*JQo)mPAmckpZ zX6izpx@{`n=#s57mUafaTif`y{-&d?o;EhBaQHSuEIZBnbm+By#nq#x*r)E&>HzA| z#Gfj!g^tgsZYrdJOQSxM&{V(TYN9FjshcMVr#Sft@kc$HIIFzoYG$?gr)_G0CoN_9 z#iA3?-jcyb{kW_BA6bszAx8_U4KFrjf58cjUm$o z)KgE`GJ(6IUt5iuVOzv&1?jf|%|I7PPIwAQL>&!-4TiOKmU7h7duVOYP#rW()#fm8 zny5B>#}+AJg*HZu*0ZbCg^t_Wf!kUwzu)G*cCU^5QZ}}GZS*tT+3vM{&wOipu(qlR zV_=Kb%N_0ZG}AtFJwofB;m&@q?R(~1`#rYRySMi|kdclgtM$(R@dFYh7rxy>l}Dk= zI4xH(AGh~l2IMT+EU9Jk3Pogop4=7uSZB2OzAEimO3u#E}3P(%$H4-ScKneIjf@qpLpK4^)1cb60i zNTfGj>7H;LKgNefAyNLgzCmti7V(tF}Xx#Xrap|N-> zVeXp9({0Kss^MVhxHURN1;SCT=sJvlQ&yPXQ%G@QS~lOJDdCjGFVPIL&LFG`n3IG( zpfgVXani|&Ar&PhBo-BQ!MZ$@V7`P%T%1DIj)E1Ddb*4<44Cp_t^2hc3pD;)oBU@~u6~diIeUARE7(XY)@M-jtTjguZFirCb z;}g~Rj>Q)N^gIMcd?TMz6iIoJAqdET)Ez9)d=uRfHEZF#YcmT|&2XP)Y{}AKt?%0( z8e}H2k}=Nl91G2rYe0O)$?e!ys$`9ZtSem_3y?d@X;{2ZOmT8c*Mw~xXDP=>K!KwH zg^dW*3$ypOtbDoC&Gip*-X|i*^Ms0(lv+w2DvW0ilm|j@^%|(ix2Qm`wdOnCs^%R( zo&51{|Nl`ce%**)|L^}1zpllv;@9=Bt8W|$$OjD|d66{E7)`r~efx>@M^l)|@FOZI z=xMr4h>12*iQEM#S%87G?E8%Iyo^ZdCFTG8s^HZ6{>``F{qd{6J$+=?8h^e3=7>v{ z23zl)BK}Y-(TW@|m*I-S!b+^-3FljNbZ0CW3B}k*#v%i40|z6`_73XNKn({ zZu82TrdM&YN1;{9YaGVXhRA4~kii4}RGk`{WdMpgWjTrxrsHMx!nm85WDj=dpX?{1 zsV=J~9-7je;-n%T!C;{b6wuY8)HJ?H1(H)MTOfRoX#&Z0Dg_-^j_h(HEs9kx;zTJf z*|Ib)s_IS>Munj#QtNiCqweYh$W_M4iBsp#PHTOf5st_`;oC~8B4Df1Td^bUE8i2ROm;7rO-Cl$CEK1u(0y);QTtB)3?&|FHb_8Lz*hPd+}K zKCcV9YR$Rl5L7!GpW17BQw5>06|ysfXRBdOEvcsXyuF@H%3#K-okSdKKBqEK<8jsO zsHWySvh6y%%S%}p^t=JK?U=-J2xGLM36W~Dl3N$}Lo>1O>?8Fix$Gw{^qi7iqQfP; zMB%{yki?e*g#1JaFwx^A!R#U5KFKg!a^6=$Ote!{OtjC~ss~PU^2M&oKUZq+Tb7}v zzrA%i2um{ZNF9Pl>T9`QUbDP36@U*89m!QVt)Lt`;A|uH(@6a^Qa_EZg(V zX{3I#UC)F1iC1~s2gQ)oh~F!5B!==Da*gf;F)cD%kej-|mrq{6e_Ee$uihEqYup{uTaOAe&svXYRW5KU%7Q!e=;+(iKRC%rx)qEE z=Dk%~P#y28i_AcVu7ZAwaF%a6@g2mZ&nPzny5m3HTo25HQRqcbJBp`^)~whAG=atP z$Smvh>E>GgvcjEk4w&)C1IjU9lA)Ck7`}@BeV6g83FIvUSg1Su8NtOdN}rol;V0mR z%o)NdWHLD9$kksw;GlJ;#_2zryOssHIRsjxKmB-HIi8|#PtWB0oa7sxlK5C9hj^fZ zz|B8YZw|~C#B9U_EjdhOr&i|z=w-e3%TY0OEQ=0pZ`D96b&XI${j_r}oP!c3k14ht z&O%0H@p^4Isw`Jv5#MbrLP3TOB43pT)MAEJr;S{WI2Aw-JKsb-PE&+a^w){5XFO3g zJs63DuCV-e?@?>*f>t*`18RyYf6ymmM+X^L8TeqH?w9^I%vh$NNJAZ9A)S*h1JyI#gXS5rts15wC>IV^^T@Js}*{2i1 zmf7qsShx+5;{G5{#wA0aLw)Q}F<)0~^2yk+V#wMnXP9PL`O*3B-+= z#X%*7)f|dX5{56U*Egv$*xzZQfwY}Og3Oc4j z4i!+*pVE&ce_;IXN6uMDcG|a{FWXES`csa>jT#tqQTi!Z6m7Up=$UhziZ#u1<9Idl z%bBB0SEQ_OYpNrtuF{fRrk&4uO34*nfXp<;gy+HW`jYu^H%~Zh_<=z2|JigRM0 zs7w`=laei(->-O;i}(CCwTdE-UTnNR?uAGYQcO|V-SXvVrgk6N#aZK81^uO!%NWgB+}V$BOF!e^Cue_e z0f-!tsy(nHle0n7Cd^bHS$Y+db;fd%<{wDD>VlIr_CNMl5sq?t53vvBT)61;oTV(C z(4q*mhB5>?n*KDqJx13*UHQgIo$f;P-AlG(sK)lEnEuLi2Jyomb zb`27zGpRa)&L*$_2E*vTU;WR29#8(}2rk$$D|YS*5P2?ZLNg8oHX)m6rB;E)zKzxZ zU#dfLU*hq`d~1SC{^{pg$2mg2mrXfY&_O5fS^0Y9OaX%n`U7Y>JA7uqdkExT5oVoRb5tPBKMCt#yPJ1BxnEMcNU})yjE5}ku%^Y zP`~`{H**=yjd(0hN{#-nQi<~@6EC-ToeciQ>FMd|oAYz{@9F7j^S|G{dUN)>v)8ZQ zyg5HR|MtzR-<_U)_vZE4@6hR9TiS7LM2`9IPIvxRvvFU@HJMC)k2Crg!UfDBIHTlq zPEr98(|3Oma(cTz`~4kF)`pT0c%;)-w}`GY6&IooAK+9nQGfE$ zvijB8e9w02JnA6xDL!dm_$mrnoA2HG3Mo4ER&5Q`oj_tO?PBN32~a9kMU?6_pt-`$ zz|2~}HX@eZ6P_bZBDPHFFGbnHFc>5_Cy*}($VU@(ggmxHFRt-cXLdrHv{D|t&@xn7 z`e#XVGfWGHq!So%biTkSnxfBz>?(FfUB zrJ|P4_efYcYWQe+886G#W&cruB?6*SGRE^=Oh9h?&#Tk(R}K5mtJCv0=cE1SF|KE9 z|7ok>P!o_o9^RUg8djj@?Uo^^Rg;D-sCoa%%t6hT_$)%r=c~&Lz~fERY9&@enJSR^ z|5X?}Ojy&V0lvSrJ#71cA=N6up|a9Z_crGc@stU}@nnt@oJKTVo;3S-v|$z3Xu}$9 zSYMhAt2y(9n6X^5rRzjdW&TklOP0-gJ@-!gjiPe6U?o-jY_LDJ@9oO(3ZGBsL)VwDknDFEk^iK6G4k0eOHMVnVSNmL{EDOb%LDQuUp!4<4yVeUf z>LsOGW+t*!c44?W%mU->CHEoS%kccjaf(&aX1(t!t+^^Bk*O&4O`6jL;f@1)#Y}6% zw`c~yf&seAJp1UO*HqKkKBqpps@JKR6+lz^OT|_3*PYyIz?%yZzjuUW6P(cdnn7J> z?pR}*!p0rVRp!PhVWOK@gXV%Hi)Ye*SvkO;Mc+-!`j~z*0l11dUUixLUCo~U@l>$( zDzam`0GbR9*UA})z{DTwG%U|$ba8@Xsm;%+hi>Y1a`h6Vu1`AiFhwg8-D!(wrdX}; znw0&8IRIqfBXBNN_u&Mjx~ZdJZN}EiYpDv)rdO5nDF&eD)whGh>RWo<(dYui|v#-{m^S<(j5}6)zsN<`8)Fif4py zd5HaVbLFY|w0yE2EKf9(Gm>{2q)1ClQ|rtyyZ8vt#~W^@k)ohJ1H{@W&RBkV;X5te zsHZIF^#;+jIoLCfF2Vfx0cW;zlWBWFb={`9?sLKo2~kmPs5GIln5rX*rizA%y{NlF zcBnvp+-R(QM!s60Im=gNfsog(Qeyep3iGS7I_ozo6~lK#piJ%uV)=vZlRmD{$b7lW znHtKyS~kH(=(m&5kjWfZl6aS;tv8}z@J6_%ZkseFyv%u{_{>!oDn4^`M>^3n-o1l6 zS3s!P97zNU9odJ#bWu+vEf1k&ZKt6Jed$cAUlX(@LV$N%kJ(7sdsq0LsKHekT1~C& zINR_|LcT{||9>kij^KQ%I`#wN%u^21geBHxPrsvQwGpL46rbR9nLHD8^6 z_m*!&u2RlFdL$2$rxf$`05MeR>JkSOsi3`F- zA`M78jn^dJF;2+#P3;ORi0$JdgY)CY3*}KZ#s`h@L1TQ-Z#O4G_2Y5-v;r848kmKm;K7@d{DF|P>i^C-n_IIug3BjWoL>Z`iaN3C zeUl5xFNCw>c5w(>sqW>NP~kA;+M-6+QMBQlqeZ@?KOHd&G0)OI2QQWlo8 zh$Ubk(aFNnB@$ball4oaX&;Nnz+78AW~(Q>sA+Cu2CigXjY?YuqRUiqMpKE5BVtEghSX59mZLe3JpZ` zmL%BXw0#6+zim{au_DP|jJE3#B=D@;^xTwWGJO_<;#m9S<=$QYN@C?mTVbK2XKV(x zTofMs2xoXs6PjDBp<@#P;_`lI(iCcIttAV=}W-hPm~99u3Ln7UWoj4!j9+Ip$j;@9&)Ns&r6#i0;H; z9gg~Tg@-!pQCLRoE~kB~B71b)qe0Gl6p|P?XyW%L1ok@fTjJ^C(08pk4p)Cr`-Y`o z9peEGb9MRu^F*j=b4Tvi-}(Ljug=e3HThppU%wvx|BrD!WB-3e1&4b5Z?9)RWrY=| z^z|3Nw>+Eie7WC!x9YvB?y9x-UVEK?ed=`A+O2+l8AYbHGs@0HK636OB6SdB&O6*Q zn3MA8N;j90i|O`_UT|FoncD%WPP_dYejD!>WrQ5{| z{MtCK00%BXjNzOy;|?GHhkRQxjRrBlrcMj5+B3_&4i<`fUAB%_Z_LqR8bj)*H=idB z3_@Hp4qTKWBQ(;JqpcX74+*6{hIo!* zP_=kfY2pje_7=^u52f}>!|&SizMQ>wLLz-;0mlyhfleg8Z*kkH?H_nY%=)HSF+5+N zJDLVQx|vQN?N#fIe$wYw>8I=GA=zLOBKYiFzy8NUpT}wLV(v{rN=Bq}%V{EALZ9@( z=eX4N=KezlI4|SuXwP`y!VOrHB-zo%eK)MCy`f#x%N!#sAp*Zv&x%}gSfsBH*1}Z8 zUm}Jn>8PC{8hA}O7FECy?8%&H^BGy?6OMcK-ml1fRLs`)?5r#b$dgd{!)4jkY*S-? z6xfMU&vst(2+`gtO$&(h$epCsFyQ|tfH?SI&%I^y*kM6KM|jqAA1gu!Gm&ga?#E-;+`-l*yX!I6=Fi&~x>C1zmWa*Zf97+FXtBM}WjCN`mxKbA?- z!FDSjy?@_3!=RM9=KkTU1q0LN!{814+-Xq{-w@HL&^^_S^hQ9~a;GKLB1Z9JWyudT zCmWp1=oDi3#aTlqL_F9Y<2LOH@T6^Oa>$(&gB=Jn#J>Bk@8re*h!yQp;CIk0_RLq_3<9f&e=AEiR0TqqE zDpe7iQc@+SMk)KR8lRecl5z#|e7O)+9pL#xs)KD1#|@&CV)B8gV_If@I-3bF2cl|M zhF@gScYY?U>reIo+#!7c7CfzET!?kr6Ycq1s}$wguZmcAq?qwaKA3WWWs&5V&!-|^ z*DkjPPms@NYhme;d;!@fx^%U$ET~9y*_)=^zLLmK$x7nV=FuY$!3&N7%4} zEfaXUDxs{f5C;ycJ_KZ0qmqc9lnkpvjmfixlc%cqsoXw8w%u>b9g7g5SLHJpF)sN! zXJyKs-SIa*cjC}?b2Z_+mbSDk=;59z>!nyiva{f);T1G^{{r(&wqXWCS!L4=#B+aJ zAQ>v(&v>vB!>keIyz<5}qcHhCz^=*B6qWaf2KW@kJoZ%41D~{%{4WYj>tteN=;N73!@L8kDXVfYO+l_Ux;R;`Qb}y4xG)l>zZ`86L`L}Ow6N4oPCPZ$0<4ORSMp%dY3|oTK!z+Ci{6}Z0AsAAnG*;oS$MenK z@AGa^4u9T%-#+(opW7-@d`c-n!&v5N7&awJmtjhl{l#b622@L^4HI29l+v)o_^)Hb z(oM#r>Dj-nK7BRdi{cm;umLz%pe#c1CD#Swff@{ohLVoZbqTO%)jAL*+w_j405_S8 zvho9A27*l9`Z}#vk-_C{EMAYIBStqoqnAZ2UZLwVo*6L>gn6+{FYwe;pi}glG%}n@ zFJlyHKTRvPDO{40fcZN_=O%T3Z6EMAz;1ro&*WArQ)m7h9T{g%)bFS~ry#Nj;^FT? z2*GXfe#O_O_T2`3Zlfutd%Aq)oe8k&D$s8hb=8)!!gzU46-h|zQfxk_Mxd;f>`3be zZ_H*e*o4Af>rTDHlowUo87Am9zZ&U^9@X;3q*eweLoL-@y^X&%6P7Midr{%T@?Qimr(P{G3oT{Yhjrp)yU!<{z-M5?7g^y1BFXoNvcIF}+H4a2?d!IbE5 z8xIkkUT!+BhFy|gltq?d-(wALTkhstXRX|qs;!gwx#HyxLuNFo`!guv6Ap)Ecp@>|Vpbtu1>)^gTJNIc$C0!7jWOuqiuSHEwl**!>m(u#o^CY zbjQyGB7p-R_j{s6jDFV=h_YOQ?cnkU4|zT_Ld!QtB&-^&&0j=+)M74$+GWsb z6|ci6M@Vp(WYNv@S4_Ya)-=UG?CoUTsHX*^+Glp7XsoiYI_!*F#Q52BKD>`$@Y47Fx<05(o%XJuC{&n!@w>W-C8?V4ghRF8bhZ}6wp zgl>oIQKEOvo7&;p%N*FX^<_`2uoXHvQL(2hVhR~$$6CsgC-t?^2wtw3W0}}dq+z}S z4R{_G*FwVA>UhH-`jVgOJ)$*l#+8X)l53C_ur4}JSJK(JxM%fN4oi=}>} zjsri1)}ym|(w6#Vad82z$nX$%8a0PKGoRP7(=^d9Z@v8Dtke+$*n>lzX*}%PbbTI} zrRon6lMQpGqBb*Es`oKl{$opVTuf`}tY#s|q)1!jx=b)v%7gN~83mRcPNe*QzLa4s zU^Rrzy7wA!P~QV%GF7LSB={3TKItLL7WYd5J&$)XW~d%aYXq4hr~pj5kA*JUm_xb5 zOS_E2+tN_Au2btA%xpbwB8S9Ogj(t!UR2r_{m>?y_-XNeeKq%36SSSq_`)(nuh0Ak9Bt}$Td*? zaa8Ne=u&E9|89ALT^z(ax}6Nj70i5|ljZjrK`Me7>U%Df&pxO$M?C`iusA~4q4mf4 zcUxSSV85W=@jf_js4VQ115{v!|IV*^Cj%&;KN@+6hK?4^!LV9@GBz%sS_+&e~aD^-PK?$$FKw6ypk7z3By zca0j$#I`X%t~DE=_M9ot-y6j?qK|zCYHPLYX-bb0z7WY<)3R(%qoy{I4I0&ay-|TS zdkOQ2-I7awCujSDaEtise9u4j6r|>1Epmm5;4pC?Z1O3?mDK`1PB)|8Dx)Id&H-Hg ztxt*<^Wbn1CA8W;6Rj|o*pePx4}pAaxnFtJrL+gm(&T?LbI_Q73-H;ruS@?6!iDO^ zxc;H9hYNuwfBf6~9I#aV4}&zqtp63#{Vw~LsuA{Ae1U@WA5=r#g6pB%BKwQ_KjF8( zB+R?ca3QVPXJL)7Dm!U%Pb1ocpFV%rwr2JHSMVCF5w={U>K>^vjFm(^;i}}`OC`iq z`?f$2YbQteqQu|U?z@Hko)j^-n?MB zj5P?Brp&)w^=V93fnkZDxag%;$nUh20fDdLaxD(HDh3~A9*zNXu^s5de#(G*J+a~vaUq*SN3*Xhn?f&!Fy@lx1f9nUlgZT1#IOOGHRG)zYEK zR)ue@rEm8ba;_CHBnM0C=T$_@eAq6OD%KMxu`z9#npnk`7<^O}zChuCB{7Y>W(zx_ z@wcyh*Ko_C9T!p~!t#T!Mhf=p%0zzAsJ5{y=3?F9&UQeQvuPZ~!}$Yjf*|m8`r1%HC)h))D8s%SJSP&T-~6SW;pkc1#@ynlqB#wV(V3LmoQp?U&V``C986s566f%WD8gn7fx#HC%6drV9w5U zB|}u~>p=N6QU3^Mo>Ls88B^S@aRC0(u*M_`(Gn#Ih>oj{e?^$DnNmmqL_`#z*e%}--jh8=-OP299$34t4=bySz)mkS3%=h4|rExV*D>`CilkDS@4!9`_Od5orKnTixF$ z{D=$W#YT&BTaST;n^W!xV0wVUg|Rc4{N^_OnQrT^Hxa4hVH^Xd!aDEdQ@M$Uh4-5Q zH6ZqC#@%QGkry%HG7pB80mJY9d_=wK)&wFdd$i?0&_|jO|MOjnbkBx(kHW{Nub5Tt3XKt)>BGB;^CX>S~cW&M#0Xs!O%xV)o-4!*> zp|+{bR3AUBn#WqX3GW=^khcolTH-5^c%iy1DR*=?>U?|~#=R8^;QeD1@CRusibaq( ze*aT0N+l1)fg%6>Z=6!OKY$j@eOmUB-z{vs<9PBUfILt$%VnU7=q#Hrz+}r@oCerL za%K-}%Zu{`h@;sbPhA#oXuGL+Og3tHt z`6W*9IgN=^ZSKTzU06j3XGyg#9=s399~iYrs(>t)*p*=6EH_!0VDa;Dew99NOrA?1 z5mTq?IZH>f zTT3vWxLuR4e*Pz9A*4c7_-}RQKh?IXU6ty+1=_I7Q*)#Ya;Dx9ZUV6Bdla-cWNh?K z=7#I$dC>Oi_}X0@7SBMAS_A~M4u^2|H!L-~SH!4Sev+NpZl|-kHS9TD!NEQujJ-*r zURYR%=(uX~9JAEj%|6J^FV)XOUpk0T@6Kg^tV0fdUut^1ka3ARV2fqau&!-1rH64p%_&7Bm>=hyctZ{ya}I)Dw-+ zRr(4O4bL?+4{2oBv`b6`bt=c8b9^)S7OWRPPTk`RK=l2_9csnp7IGHMxI!Cx4C1ry zryZ&h&l3Mc&Lyy$Ybx0Ga_d_95VaeP;4mBDlx;8&6To>EG7qBbq-Uw8rz`oABlTN7 zh(`@1iNEKIf$@2tjHBR>4+Yv0zZU&3w^w)+Q))15QarbxCh5?Su3+TOYpHiv1Q&QE z50Bz${u!sBj|LRX729Zt`T$>kmhhr?z?0X`+X|98_gzc~rc947QgVML2j&t42q>yk zDVCHWA>~{Eg@cwW<%oQcGA~f+(F`yVv+9>oM6G_x@3Gx+1ksEON^j+MQCG2G86i+F z=TR+7P<5OFoz}vvtXn^dbZf!)0pKf&g>|$8=acWPh=M|r0-&BpiP`z2<*(NI+SiR& z$EmB6-gSU_3roLRzGKd)L6qV%1qOU8DI!;$a}7!uyJVCtB=N&JmZn2y>I_?7nVt^K z=AqjkAI*(2Ij-9c(!ss;=UW9!mg_bl+Mlj~a&S+M%g4)i*x@C~ zTF1tt!jO(H{T8eyZ#6fStwLV}+@jm4{hGT6I7r3UbK>l>0hf~LR!oCFG}_J%)bakd z7hDwnZ65_&=rF=*R#7 literal 0 HcmV?d00001 diff --git a/charts/gateway/values.yaml b/charts/gateway/values.yaml new file mode 100644 index 0000000..d83c9d6 --- /dev/null +++ b/charts/gateway/values.yaml @@ -0,0 +1,77 @@ +traefik: + image: + digest: sha256:1336e85aaed293bd63d005086e0dd013b04078b26aaeb1c18b26a2f928af7e65 + # The digest hides the version from the chart's version checks. + versionOverride: v3.7.16 + fullnameOverride: traefik + + global: + checkNewVersion: false + sendAnonymousUsage: false + + api: + dashboard: false + + gatewayClass: + enabled: true + name: traefik + + # The shared listener: plain HTTP on every hostname, open to routes from any + # namespace. Hostname and HTTPS listeners belong on a Gateway of their own. + gateway: + enabled: true + listeners: + web: + port: 8000 + protocol: HTTP + namespacePolicy: + from: All + + # For cert-manager's HTTP-01 solver Ingresses. Not the default class, so an + # Ingress without one stays unserved, as it was under ingress-nginx. + ingressClass: + enabled: true + isDefaultClass: false + name: traefik + + providers: + kubernetesCRD: + enabled: true + kubernetesGateway: + enabled: true + kubernetesIngress: + enabled: true + ingressClass: traefik + # Serves nginx-class Ingresses with their annotations, for apps not yet on + # the Gateway API. With snippets off, an Ingress carrying one is dropped + # whole; on, Traefik runs only its small directive subset. + kubernetesIngressNGINX: + enabled: true + ingressClass: nginx + publishService: + enabled: true + allowSnippetAnnotations: true + + # Traefik v3 cuts any request still being read after 60s, which kills long + # uploads, streams and websockets. + ports: + web: + transport: + respondingTimeouts: + readTimeout: 0 + writeTimeout: 0 + websecure: + transport: + respondingTimeouts: + readTimeout: 0 + writeTimeout: 0 + + service: + spec: + type: LoadBalancer + externalTrafficPolicy: Local + + resources: + requests: + cpu: 100m + memory: 90Mi diff --git a/dev.sh b/dev.sh index 49919d7..7112d8d 100755 --- a/dev.sh +++ b/dev.sh @@ -56,7 +56,8 @@ export KUBECONFIG=~/.kube/5stack-dev kubectl config use-context k3d-5stack-dev -install_ingress_nginx +install_traefik "$PANEL_DIR/overlays/dev/config/traefik-config.env" || exit 1 +retire_ingress_nginx || exit 1 echo "Labeling node..." kubectl label node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') 5stack-api=true 5stack-hasura=true 5stack-minio=true 5stack-rustfs=true 5stack-timescaledb=true 5stack-redis=true 5stack-typesense=true 5stack-web=true 5stack-dev-server=true 5stack-mediamtx=true diff --git a/fix-ssl.sh b/fix-ssl.sh index 652ff1e..6b2ddd3 100755 --- a/fix-ssl.sh +++ b/fix-ssl.sh @@ -28,7 +28,7 @@ while IFS=$'\t' read -r NAME STATUS _; do continue fi warn "$NAME is not valid, requesting a new certificate" - # The Secret is intentionally kept: nginx keeps serving whatever it has + # The Secret is intentionally kept: Traefik keeps serving whatever it has # while the replacement is issued. output_redirect kubectl --kubeconfig=$KUBECONFIG delete certificate "$NAME" -n 5stack diff --git a/install.sh b/install.sh index 7b2f026..cd95954 100755 --- a/install.sh +++ b/install.sh @@ -61,20 +61,11 @@ DROPIN systemctl daemon-reload ok "drop-ins installed" -step "Installing Ingress Nginx (this may take a few minutes)" -# Two failure modes, told apart by the exit code. Exit 2 is the admission -# webhook still being slow: the wait exists to avoid a race, apply_overlay -# retries webhook failures anyway, and a false negative here must not block an -# install that would otherwise succeed. Exit 1 means the manifest never applied -# or the controller never came up, and there is no ingress layer at all -- every -# ingress in the overlay would be rejected, so stop here rather than a minute -# later with a message that no longer names ingress-nginx as the cause. -install_ingress_nginx true -case $? in - 0) ok "ingress-nginx installed" ;; - 2) warn "ingress-nginx admission webhook is not answering yet; continuing" ;; - *) die "ingress-nginx install failed" ;; -esac +# Also what holds the node labels below back until k3s has registered its +# node: Traefik is not Ready before it is scheduled on one. +step "Installing Traefik (this may take a few minutes)" +install_traefik || die "Traefik install failed" +ok "Traefik installed" kubectl label node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') 5stack-api=true 5stack-hasura=true 5stack-minio=true 5stack-rustfs=true 5stack-timescaledb=true 5stack-redis=true 5stack-typesense=true 5stack-web=true diff --git a/overlays/cert-manager/issuer.yaml b/overlays/cert-manager/issuer.yaml index e6a446e..4d47d1f 100644 --- a/overlays/cert-manager/issuer.yaml +++ b/overlays/cert-manager/issuer.yaml @@ -12,4 +12,11 @@ spec: solvers: - http01: ingress: - ingressClassName: nginx + ingressClassName: traefik + # Plain HTTP only, and above the https-redirect route on the same + # hosts, so Let's Encrypt reaches the solver instead of a 308. + ingressTemplate: + metadata: + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.priority: "1000000" diff --git a/overlays/config/kustomization.yaml b/overlays/config/kustomization.yaml index 2be8962..d3e5f58 100644 --- a/overlays/config/kustomization.yaml +++ b/overlays/config/kustomization.yaml @@ -14,10 +14,6 @@ configMapGenerator: namespace: 5stack envs: - s3-config.env - - name: nginx-config - namespace: 5stack - envs: - - nginx-config.env - name: redis-config namespace: 5stack envs: diff --git a/overlays/config/traefik-config.env.example b/overlays/config/traefik-config.env.example new file mode 100644 index 0000000..01e9d86 --- /dev/null +++ b/overlays/config/traefik-config.env.example @@ -0,0 +1,4 @@ +# The ports the node serves HTTP and HTTPS on. Change them only when 80/443 +# are already taken on this machine. +HTTP_PORT=80 +HTTPS_PORT=443 diff --git a/overlays/dev/config/kustomization.yaml b/overlays/dev/config/kustomization.yaml index 2be8962..d3e5f58 100644 --- a/overlays/dev/config/kustomization.yaml +++ b/overlays/dev/config/kustomization.yaml @@ -14,10 +14,6 @@ configMapGenerator: namespace: 5stack envs: - s3-config.env - - name: nginx-config - namespace: 5stack - envs: - - nginx-config.env - name: redis-config namespace: 5stack envs: diff --git a/overlays/dev/config/nginx-config.env b/overlays/dev/config/nginx-config.env deleted file mode 100644 index c165cec..0000000 --- a/overlays/dev/config/nginx-config.env +++ /dev/null @@ -1,2 +0,0 @@ -HTTP_PORT=80 -HTTPS_PORT=443 diff --git a/overlays/config/nginx-config.env.example b/overlays/dev/config/traefik-config.env similarity index 100% rename from overlays/config/nginx-config.env.example rename to overlays/dev/config/traefik-config.env diff --git a/overlays/dev/gateway-patch.yaml b/overlays/dev/gateway-patch.yaml new file mode 100644 index 0000000..625d72c --- /dev/null +++ b/overlays/dev/gateway-patch.yaml @@ -0,0 +1,89 @@ +# The panel's HTTPS listeners on mkcert's wildcard certificate (dev.sh), plus +# the dev-only Hasura console and BullMQ dashboard. +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: 5stack + namespace: 5stack +spec: + listeners: + - name: web + hostname: ${WEB_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: api + hostname: ${API_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: demos + hostname: ${DEMOS_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: ws + hostname: ${WS_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: relay + hostname: ${RELAY_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: typesense + hostname: ${TYPESENSE_HOST} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: s3-console + hostname: ${S3_CONSOLE_HOST} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: game-stream + hostname: ${GAME_STREAM_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: hasura-dashboard + hostname: hasura.5stack.localhost + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: queues + hostname: queues.5stack.localhost + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl diff --git a/overlays/dev/hasura/httproute.yaml b/overlays/dev/hasura/httproute.yaml new file mode 100644 index 0000000..ea09825 --- /dev/null +++ b/overlays/dev/hasura/httproute.yaml @@ -0,0 +1,13 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: hasura-dashboard + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: hasura-dashboard + rules: + - backendRefs: + - name: hasura + port: 8080 diff --git a/overlays/dev/hasura/ingress.yaml b/overlays/dev/hasura/ingress.yaml deleted file mode 100644 index f160e44..0000000 --- a/overlays/dev/hasura/ingress.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: hasura-dashboard - namespace: 5stack -spec: - ingressClassName: nginx - tls: - - hosts: - - hasura.5stack.localhost - secretName: 5stack-ssl - rules: - - host: hasura.5stack.localhost - http: - paths: - - backend: - service: - name: hasura - port: - number: 8080 - path: / - pathType: Prefix \ No newline at end of file diff --git a/overlays/dev/hasura/kustomization.yaml b/overlays/dev/hasura/kustomization.yaml index 2d6fedf..63b30fe 100644 --- a/overlays/dev/hasura/kustomization.yaml +++ b/overlays/dev/hasura/kustomization.yaml @@ -4,4 +4,4 @@ kind: Kustomization namespace: 5stack resources: - - ingress.yaml + - httproute.yaml diff --git a/overlays/dev/https-redirect.yaml b/overlays/dev/https-redirect.yaml new file mode 100644 index 0000000..cdf1f9b --- /dev/null +++ b/overlays/dev/https-redirect.yaml @@ -0,0 +1,29 @@ +# No port on the redirect: browsers come back on 443 whatever HTTPS_PORT the +# node uses, as they did with ingress-nginx. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: https-redirect + namespace: 5stack +spec: + parentRefs: + - name: traefik-gateway + namespace: traefik + sectionName: web + hostnames: + - ${WEB_DOMAIN} + - ${API_DOMAIN} + - ${DEMOS_DOMAIN} + - ${WS_DOMAIN} + - ${RELAY_DOMAIN} + - ${TYPESENSE_HOST} + - ${S3_CONSOLE_HOST} + - ${GAME_STREAM_DOMAIN} + - hasura.5stack.localhost + - queues.5stack.localhost + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 308 diff --git a/overlays/dev/ingress-patch.yaml b/overlays/dev/ingress-patch.yaml deleted file mode 100644 index fa4899b..0000000 --- a/overlays/dev/ingress-patch.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- op: add - path: /metadata/annotations/cert-manager.io~1issuer - value: 5stack-issuer - -- op: add - path: /spec/tls - value: - - hosts: - - HOST - secretName: 5stack-ssl \ No newline at end of file diff --git a/overlays/dev/kustomization.yaml b/overlays/dev/kustomization.yaml index 04e5815..0c07c7c 100644 --- a/overlays/dev/kustomization.yaml +++ b/overlays/dev/kustomization.yaml @@ -8,6 +8,7 @@ resources: # - dev-game-streamer - queues - hasura + - https-redirect.yaml replacements: - source: @@ -16,192 +17,165 @@ replacements: fieldPath: data.WEB_DOMAIN targets: - select: - kind: Ingress - name: web + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: api - fieldPaths: - - spec.rules.1.host - - select: - kind: Ingress - name: web - fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=web].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.0 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.0 - source: kind: ConfigMap name: api-config fieldPath: data.API_DOMAIN targets: - select: - kind: Ingress - name: api - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: hasura - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: api - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Ingress - name: file-manager - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: file-manager + kind: Gateway + name: 5stack fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=api].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.1 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.1 - source: kind: ConfigMap name: api-config fieldPath: data.DEMOS_DOMAIN targets: - select: - kind: Ingress - name: demos - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: demos + kind: Gateway + name: 5stack fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=demos].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.2 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.2 - source: kind: ConfigMap name: typesense-config fieldPath: data.TYPESENSE_HOST targets: - select: - kind: Ingress - name: typesense - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: typesense + kind: Gateway + name: 5stack fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=typesense].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.3 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.5 - source: kind: ConfigMap name: s3-config fieldPath: data.S3_CONSOLE_HOST targets: - select: - kind: Ingress - name: s3-console + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: s3-console - fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=s3-console].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.4 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.6 - source: kind: ConfigMap name: api-config fieldPath: data.WS_DOMAIN targets: - select: - kind: Ingress - name: ws + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: ws - fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=ws].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.5 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.3 - source: kind: ConfigMap name: api-config fieldPath: data.RELAY_DOMAIN targets: - select: - kind: Ingress - name: relay + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: relay - fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=relay].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.6 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.4 - source: kind: ConfigMap name: api-config - fieldPath: data.MAIL_FROM + fieldPath: data.GAME_STREAM_DOMAIN targets: - select: - kind: Issuer - name: 5stack-issuer + kind: Gateway + name: 5stack fieldPaths: - - spec.acme.email - - source: - kind: ConfigMap - name: nginx-config - fieldPath: data.HTTP_PORT - targets: + - spec.listeners.[name=game-stream].hostname - select: - kind: Service - name: ingress-nginx-controller - namespace: ingress-nginx + kind: HTTPRoute + name: https-redirect fieldPaths: - - spec.ports.0.port + - spec.hostnames.7 - source: kind: ConfigMap - name: nginx-config - fieldPath: data.HTTPS_PORT + name: api-config + fieldPath: data.MAIL_FROM targets: - select: - kind: Service - name: ingress-nginx-controller - namespace: ingress-nginx + kind: Issuer + name: 5stack-issuer fieldPaths: - - spec.ports.1.port + - spec.acme.email patches: - target: @@ -224,50 +198,7 @@ patches: - | yarn build && cp -r /opt/5stack/hasura /app && node dist/src/main.js - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: api - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: demos - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: s3-console - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: typesense - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: web - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: ws - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: relay - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: file-manager - path: ingress-patch.yaml \ No newline at end of file + group: gateway.networking.k8s.io + kind: Gateway + name: 5stack + path: gateway-patch.yaml diff --git a/overlays/dev/queues/httproute.yaml b/overlays/dev/queues/httproute.yaml new file mode 100644 index 0000000..015f511 --- /dev/null +++ b/overlays/dev/queues/httproute.yaml @@ -0,0 +1,28 @@ +# The dashboard lives under /queues on the api; the host's bare paths are +# moved there too. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: queues + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: queues + rules: + - matches: + - path: + type: PathPrefix + value: /queues + backendRefs: + - name: api + port: 5585 + - filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /queues/ + backendRefs: + - name: api + port: 5585 diff --git a/overlays/dev/queues/ingress.yaml b/overlays/dev/queues/ingress.yaml deleted file mode 100644 index 42f9e0d..0000000 --- a/overlays/dev/queues/ingress.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: queues - namespace: 5stack - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /queues/$2 -spec: - ingressClassName: nginx - tls: - - hosts: - - queues.5stack.localhost - secretName: 5stack-ssl - rules: - - host: queues.5stack.localhost - http: - paths: - - backend: - service: - name: api - port: - number: 5585 - path: /(queues/?)?(.*) - pathType: ImplementationSpecific \ No newline at end of file diff --git a/overlays/dev/queues/kustomization.yaml b/overlays/dev/queues/kustomization.yaml index 2d6fedf..63b30fe 100644 --- a/overlays/dev/queues/kustomization.yaml +++ b/overlays/dev/queues/kustomization.yaml @@ -4,4 +4,4 @@ kind: Kustomization namespace: 5stack resources: - - ingress.yaml + - httproute.yaml diff --git a/overlays/http/gateway-https-patch.yaml b/overlays/http/gateway-https-patch.yaml new file mode 100644 index 0000000..f954b52 --- /dev/null +++ b/overlays/http/gateway-https-patch.yaml @@ -0,0 +1,77 @@ +# Turns every 5Stack listener into HTTPS. Kustomize replaces the whole list, +# so each listener is restated in full. The Certificates are declared in +# full in overlays/cert-manager and overlays/mediamtx-https; these listeners +# only consume the Secrets they produce. Plain HTTP for these hosts lands on +# the chart's shared listener, where https-redirect.yaml sends them to HTTPS +# and cert-manager's HTTP-01 solver answers its challenges. +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: 5stack + namespace: 5stack +spec: + listeners: + - name: web + hostname: ${WEB_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: api + hostname: ${API_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: demos + hostname: ${DEMOS_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: ws + hostname: ${WS_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: relay + hostname: ${RELAY_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: typesense + hostname: ${TYPESENSE_HOST} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: s3-console + hostname: ${S3_CONSOLE_HOST} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-ssl + - name: game-stream + hostname: ${GAME_STREAM_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-mediamtx-ssl diff --git a/overlays/http/http-replacements.yaml b/overlays/http/http-replacements.yaml index 31e02be..13568ad 100644 --- a/overlays/http/http-replacements.yaml +++ b/overlays/http/http-replacements.yaml @@ -2,107 +2,91 @@ replacements: - source: kind: ConfigMap name: api-config - fieldPath: data.WS_DOMAIN + fieldPath: data.WEB_DOMAIN targets: - select: - kind: Ingress - name: ws + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host + - spec.listeners.[name=web].hostname + - select: + kind: Middleware + name: mediamtx-cors + fieldPaths: + - spec.headers.accessControlAllowOriginList.0 + options: + delimiter: "://" + index: 1 - source: kind: ConfigMap name: api-config fieldPath: data.API_DOMAIN targets: - select: - kind: Ingress - name: api - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: hasura - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: file-manager + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host + - spec.listeners.[name=api].hostname - source: kind: ConfigMap name: api-config fieldPath: data.DEMOS_DOMAIN targets: - select: - kind: Ingress - name: demos + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host + - spec.listeners.[name=demos].hostname - source: kind: ConfigMap - name: api-config - fieldPath: data.WEB_DOMAIN + name: typesense-config + fieldPath: data.TYPESENSE_HOST targets: - select: - kind: Ingress - name: web + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: api - fieldPaths: - - spec.rules.1.host - - select: - kind: Ingress - name: mediamtx - fieldPaths: - - metadata.annotations.[nginx.ingress.kubernetes.io/cors-allow-origin] - options: - delimiter: "://" - index: 1 + - spec.listeners.[name=typesense].hostname - source: kind: ConfigMap - name: api-config - fieldPath: data.RELAY_DOMAIN + name: s3-config + fieldPath: data.S3_CONSOLE_HOST targets: - select: - kind: Ingress - name: relay + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host + - spec.listeners.[name=s3-console].hostname - source: kind: ConfigMap - name: s3-config - fieldPath: data.S3_CONSOLE_HOST + name: api-config + fieldPath: data.WS_DOMAIN targets: - select: - kind: Ingress - name: s3-console + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host + - spec.listeners.[name=ws].hostname - source: kind: ConfigMap - name: typesense-config - fieldPath: data.TYPESENSE_HOST + name: api-config + fieldPath: data.RELAY_DOMAIN targets: - select: - kind: Ingress - name: typesense + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host + - spec.listeners.[name=relay].hostname - source: kind: ConfigMap - name: nginx-config - fieldPath: data.HTTP_PORT + name: mediamtx-env + fieldPath: data.GAME_STREAM_DOMAIN targets: - select: - kind: Service - name: ingress-nginx-controller - namespace: ingress-nginx + kind: Gateway + name: 5stack fieldPaths: - - spec.ports.0.port + - spec.listeners.[name=game-stream].hostname - source: kind: ConfigMap name: api-config @@ -113,14 +97,3 @@ replacements: name: 5stack-issuer fieldPaths: - spec.acme.email - - source: - kind: ConfigMap - name: nginx-config - fieldPath: data.HTTPS_PORT - targets: - - select: - kind: Service - name: ingress-nginx-controller - namespace: ingress-nginx - fieldPaths: - - spec.ports.1.port \ No newline at end of file diff --git a/overlays/http/https-redirect.yaml b/overlays/http/https-redirect.yaml new file mode 100644 index 0000000..c7cf1bc --- /dev/null +++ b/overlays/http/https-redirect.yaml @@ -0,0 +1,27 @@ +# No port on the redirect: browsers come back on 443 whatever HTTPS_PORT the +# node uses, as they did with ingress-nginx. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: https-redirect + namespace: 5stack +spec: + parentRefs: + - name: traefik-gateway + namespace: traefik + sectionName: web + hostnames: + - ${WEB_DOMAIN} + - ${API_DOMAIN} + - ${DEMOS_DOMAIN} + - ${WS_DOMAIN} + - ${RELAY_DOMAIN} + - ${TYPESENSE_HOST} + - ${S3_CONSOLE_HOST} + - ${GAME_STREAM_DOMAIN} + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 308 diff --git a/overlays/http/https-replacements.yaml b/overlays/http/https-replacements.yaml index 82fab4c..12b0aa4 100644 --- a/overlays/http/https-replacements.yaml +++ b/overlays/http/https-replacements.yaml @@ -5,246 +5,176 @@ replacements: fieldPath: data.WEB_DOMAIN targets: - select: - kind: Ingress - name: web + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host + - spec.listeners.[name=web].hostname - select: - kind: Ingress - name: api + kind: Middleware + name: mediamtx-cors fieldPaths: - - spec.rules.1.host - - select: - kind: Ingress - name: mediamtx - fieldPaths: - - metadata.annotations.[nginx.ingress.kubernetes.io/cors-allow-origin] + - spec.headers.accessControlAllowOriginList.0 options: delimiter: "://" index: 1 - - select: - kind: Ingress - name: web - fieldPaths: - - spec.tls.0.hosts.0 - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.0 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.0 - source: kind: ConfigMap name: api-config fieldPath: data.API_DOMAIN targets: - select: - kind: Ingress - name: api - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: hasura + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: api - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Ingress - name: file-manager - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: file-manager - fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=api].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.1 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.1 - source: kind: ConfigMap name: api-config fieldPath: data.DEMOS_DOMAIN targets: - select: - kind: Ingress - name: demos + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: demos - fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=demos].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.2 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.2 - source: kind: ConfigMap name: typesense-config fieldPath: data.TYPESENSE_HOST targets: - select: - kind: Ingress - name: typesense - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: typesense + kind: Gateway + name: 5stack fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=typesense].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.3 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.5 - source: kind: ConfigMap name: s3-config fieldPath: data.S3_CONSOLE_HOST targets: - select: - kind: Ingress - name: s3-console - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: s3-console + kind: Gateway + name: 5stack fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=s3-console].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.4 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.6 - source: kind: ConfigMap name: api-config fieldPath: data.WS_DOMAIN targets: - select: - kind: Ingress - name: ws - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: ws + kind: Gateway + name: 5stack fieldPaths: - - spec.tls.0.hosts.0 + - spec.listeners.[name=ws].hostname - select: kind: Certificate name: 5stack-ssl fieldPaths: - spec.dnsNames.5 + - select: + kind: HTTPRoute + name: https-redirect + fieldPaths: + - spec.hostnames.3 - source: kind: ConfigMap name: api-config - fieldPath: data.MAIL_FROM + fieldPath: data.RELAY_DOMAIN targets: - select: - kind: Issuer - name: 5stack-issuer + kind: Gateway + name: 5stack fieldPaths: - - spec.acme.email - - source: - kind: ConfigMap - name: nginx-config - fieldPath: data.HTTP_PORT - targets: + - spec.listeners.[name=relay].hostname - select: - kind: Service - name: ingress-nginx-controller - namespace: ingress-nginx + kind: Certificate + name: 5stack-ssl fieldPaths: - - spec.ports.0.port - - source: - kind: ConfigMap - name: nginx-config - fieldPath: data.HTTPS_PORT - targets: + - spec.dnsNames.6 - select: - kind: Service - name: ingress-nginx-controller - namespace: ingress-nginx + kind: HTTPRoute + name: https-redirect fieldPaths: - - spec.ports.1.port + - spec.hostnames.4 - source: kind: ConfigMap - name: api-config - fieldPath: data.RELAY_DOMAIN + name: mediamtx-env + fieldPath: data.GAME_STREAM_DOMAIN targets: - select: - kind: Ingress - name: relay + kind: Gateway + name: 5stack fieldPaths: - - spec.rules.0.host + - spec.listeners.[name=game-stream].hostname - select: - kind: Ingress - name: relay + kind: HTTPRoute + name: https-redirect fieldPaths: - - spec.tls.0.hosts.0 + - spec.hostnames.7 + - source: + kind: ConfigMap + name: api-config + fieldPath: data.MAIL_FROM + targets: - select: - kind: Certificate - name: 5stack-ssl + kind: Issuer + name: 5stack-issuer fieldPaths: - - spec.dnsNames.6 + - spec.acme.email patches: - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: api - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: demos - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: s3-console - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: typesense - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: web - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: ws - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: relay - path: ingress-patch.yaml - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: file-manager - path: ingress-patch.yaml \ No newline at end of file + group: gateway.networking.k8s.io + kind: Gateway + name: 5stack + path: gateway-https-patch.yaml diff --git a/overlays/http/ingress-patch.yaml b/overlays/http/ingress-patch.yaml deleted file mode 100644 index 9757a26..0000000 --- a/overlays/http/ingress-patch.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# No cert-manager.io/issuer annotation here on purpose. ingress-shim names the -# Certificate it creates after the TLS secretName, so every ingress below would -# race to own a single `5stack-ssl` Certificate -- and a shim-created one only -# carries the one host of the ingress that made it. The Certificate is declared -# in full, across all seven hosts, in overlays/cert-manager/certificate.yaml; -# these ingresses just consume the Secret it produces. -- op: add - path: /spec/tls - value: - - hosts: - - HOST - secretName: 5stack-ssl diff --git a/overlays/http/kustomization.yaml b/overlays/http/kustomization.yaml deleted file mode 100644 index 0604bee..0000000 --- a/overlays/http/kustomization.yaml +++ /dev/null @@ -1,178 +0,0 @@ -replacements: - - source: - kind: ConfigMap - name: api-config - fieldPath: data.WEB_DOMAIN - targets: - - select: - kind: Ingress - name: web - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: web - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Certificate - name: 5stack-ssl - fieldPaths: - - spec.dnsNames.0 - - source: - kind: ConfigMap - name: api-config - fieldPath: data.API_DOMAIN - targets: - - select: - kind: Ingress - name: api - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: api - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Ingress - name: file-manager - fieldPaths: - - spec.rules.0.host - - select: - kind: Certificate - name: 5stack-ssl - fieldPaths: - - spec.dnsNames.1 - - source: - kind: ConfigMap - name: api-config - fieldPath: data.DEMOS_DOMAIN - targets: - - select: - kind: Ingress - name: demos - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: demos - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Certificate - name: 5stack-ssl - fieldPaths: - - spec.dnsNames.2 - - source: - kind: ConfigMap - name: typesense-config - fieldPath: data.TYPESENSE_HOST - targets: - - select: - kind: Ingress - name: typesense - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: typesense - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Certificate - name: 5stack-ssl - fieldPaths: - - spec.dnsNames.3 - - source: - kind: ConfigMap - name: s3-config - fieldPath: data.S3_CONSOLE_HOST - targets: - - select: - kind: Ingress - name: s3-console - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: s3-console - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Certificate - name: 5stack-ssl - fieldPaths: - - spec.dnsNames.4 - - source: - kind: ConfigMap - name: api-config - fieldPath: data.WS_DOMAIN - targets: - - select: - kind: Ingress - name: ws - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: ws - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Certificate - name: 5stack-ssl - fieldPaths: - - spec.dnsNames.5 - - source: - kind: ConfigMap - name: api-config - fieldPath: data.RELAY_DOMAIN - targets: - - select: - kind: Ingress - name: relay - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: relay - fieldPaths: - - spec.tls.0.hosts.0 - - select: - kind: Certificate - name: 5stack-ssl - fieldPaths: - - spec.dnsNames.6 - - source: - kind: ConfigMap - name: nginx-config - fieldPath: data.HTTP_PORT - targets: - - select: - kind: Service - name: ingress-nginx-controller - namespace: ingress-nginx - fieldPaths: - - spec.ports.0.port - - source: - kind: ConfigMap - name: nginx-config - fieldPath: data.HTTPS_PORT - targets: - - select: - kind: Service - name: ingress-nginx-controller - namespace: ingress-nginx - fieldPaths: - - spec.ports.1.port - - source: - kind: ConfigMap - name: api-config - fieldPath: data.MAIL_FROM - targets: - - select: - kind: Issuer - name: 5stack-issuer - fieldPaths: - - spec.acme.email \ No newline at end of file diff --git a/overlays/mediamtx-https/ingress-patch.yaml b/overlays/mediamtx-https/ingress-patch.yaml deleted file mode 100644 index e73651f..0000000 --- a/overlays/mediamtx-https/ingress-patch.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# See overlays/http/ingress-patch.yaml: the Certificate is declared explicitly -# in certificate.yaml rather than left to cert-manager's ingress-shim. -- op: add - path: /spec/tls - value: - - hosts: - - HOST - secretName: 5stack-mediamtx-ssl diff --git a/overlays/mediamtx-https/kustomization.yaml b/overlays/mediamtx-https/kustomization.yaml index c0d1d0e..fa37ecb 100644 --- a/overlays/mediamtx-https/kustomization.yaml +++ b/overlays/mediamtx-https/kustomization.yaml @@ -18,16 +18,3 @@ replacements: name: 5stack-mediamtx-ssl fieldPaths: - spec.dnsNames.0 - - select: - kind: Ingress - name: mediamtx - fieldPaths: - - spec.tls.0.hosts.0 - -patches: - - target: - group: networking.k8s.io - version: v1 - kind: Ingress - name: mediamtx - path: ingress-patch.yaml diff --git a/overlays/mediamtx/httproute.yaml b/overlays/mediamtx/httproute.yaml new file mode 100644 index 0000000..f5778b9 --- /dev/null +++ b/overlays/mediamtx/httproute.yaml @@ -0,0 +1,85 @@ +# Traefik matches RegularExpression paths unanchored, so every pattern carries +# its own ^ and $. The two never match the same URL, and neither matches +# /snapshot/ on the same host. +# +# Filter order is load bearing: CORS answers a preflight itself, and a +# preflight carries no cookie, so it must never reach the forward-auth. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: mediamtx + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: game-stream + rules: + # WHEP/WHIP, and the session URL trickle ICE PATCHes to. + - matches: + - path: + type: RegularExpression + value: ^/[^/]+/(whep|whip)(/[^/]+)?$ + filters: + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: mediamtx-cors + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: mediamtx-auth + backendRefs: + - name: mediamtx + port: 8889 + # HLS: the player page (//) and its files. + - matches: + - path: + type: RegularExpression + value: ^/[^/]+/([^/]+\.[^/]+)?$ + filters: + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: mediamtx-cors + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: mediamtx-auth + backendRefs: + - name: mediamtx + port: 8888 +--- +# The standard URLRewrite cannot carry a capture into the new path, hence +# ReplacePathRegex. +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: snapshot + namespace: 5stack +spec: + parentRefs: + - name: 5stack + sectionName: game-stream + rules: + - matches: + - path: + type: RegularExpression + value: ^/snapshot/.+$ + filters: + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: snapshot-cors + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: snapshot-rewrite + backendRefs: + - name: api + port: 5585 diff --git a/overlays/mediamtx/ingress.yaml b/overlays/mediamtx/ingress.yaml deleted file mode 100644 index ed0bb47..0000000 --- a/overlays/mediamtx/ingress.yaml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: mediamtx - namespace: 5stack - annotations: - nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-buffering: "off" - nginx.ingress.kubernetes.io/proxy-http-version: "1.1" - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/enable-cors: "true" - # Credentialed CORS: the browser sends the session cookie on the WHEP - # fetch, so the origin can't be "*" and credentials must be allowed. - nginx.ingress.kubernetes.io/cors-allow-origin: "https://${WEB_DOMAIN}" - nginx.ingress.kubernetes.io/cors-allow-credentials: "true" - nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS, PATCH, DELETE" - nginx.ingress.kubernetes.io/cors-allow-headers: "Content-Type, Accept, Authorization, If-Match" - # The WHEP answer's Location is the session URL trickle ICE PATCHes to. - nginx.ingress.kubernetes.io/cors-expose-headers: "Location" - # Require login to watch: every stream request is authorized by the API, - # which enforces the public.require_login_for_live_streams setting (logged - # in, and not a player/coach of the live match — anti-cheat). The decision - # depends on the viewer (cookie) and the match, which the paths below - # capture as $mediamtx_path, so a viewer is authorized once per match - # rather than once per HLS segment or trickle ICE PATCH. - nginx.ingress.kubernetes.io/auth-url: "http://api.5stack.svc.cluster.local:5585/streams/authorize" - nginx.ingress.kubernetes.io/auth-cache-key: "$http_cookie$mediamtx_path" - nginx.ingress.kubernetes.io/auth-cache-duration: "200 30s" -spec: - ingressClassName: nginx - rules: - - host: ${GAME_STREAM_DOMAIN} - http: - # The two paths never match the same URL, and neither matches the - # snapshot ingress's /snapshot/ on this host. ingress-nginx orders - # regex locations by length, so overlapping paths would route by which - # pattern happens to be longer. - paths: - # WHEP/WHIP, and the session URL trickle ICE PATCHes to. - - path: '/(?[^/]+)/(whep|whip)(/[^/]+)?$' - pathType: ImplementationSpecific - backend: - service: - name: mediamtx - port: - number: 8889 - # HLS: the player page (//) and its files. - - path: '/(?[^/]+)/([^/]+\.[^/]+)?$' - pathType: ImplementationSpecific - backend: - service: - name: mediamtx - port: - number: 8888 diff --git a/overlays/mediamtx/kustomization.yaml b/overlays/mediamtx/kustomization.yaml index 19d7720..2d6514a 100644 --- a/overlays/mediamtx/kustomization.yaml +++ b/overlays/mediamtx/kustomization.yaml @@ -6,8 +6,8 @@ namespace: 5stack resources: - deployment.yaml - service.yaml - - ingress.yaml - - snapshot-ingress.yaml + - httproute.yaml + - middlewares.yaml configMapGenerator: - name: mediamtx-config @@ -18,20 +18,3 @@ configMapGenerator: namespace: 5stack envs: - mediamtx.env - -replacements: - - source: - kind: ConfigMap - name: mediamtx-env - fieldPath: data.GAME_STREAM_DOMAIN - targets: - - select: - kind: Ingress - name: mediamtx - fieldPaths: - - spec.rules.0.host - - select: - kind: Ingress - name: snapshot - fieldPaths: - - spec.rules.0.host diff --git a/overlays/mediamtx/middlewares.yaml b/overlays/mediamtx/middlewares.yaml new file mode 100644 index 0000000..8499012 --- /dev/null +++ b/overlays/mediamtx/middlewares.yaml @@ -0,0 +1,66 @@ +# Traefik middlewares used as HTTPRoute ExtensionRef filters, each one where +# the Gateway API has no equivalent: Traefik 3.7 does not implement the +# standard CORS filter (it fails the whole rule with a 500), and forward-auth +# and capture-group rewrites are not in the standard at all. +# +# Credentialed CORS: the browser sends the session cookie on the WHEP fetch, +# so the origin can't be "*" and credentials must be allowed. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: mediamtx-cors + namespace: 5stack +spec: + headers: + accessControlAllowOriginList: + - https://${WEB_DOMAIN} + accessControlAllowCredentials: true + accessControlAllowMethods: [GET, POST, OPTIONS, PATCH, DELETE] + accessControlAllowHeaders: [Content-Type, Accept, Authorization, If-Match] + # The WHEP answer's Location is the session URL trickle ICE PATCHes to. + accessControlExposeHeaders: [Location] + addVaryHeader: true +--- +# Require login to watch: every stream request is authorized by the API, +# which enforces public.require_login_for_live_streams (logged in, and not a +# player/coach of the live match -- anti-cheat). The api caches an allow for +# 30s itself; Traefik caches nothing. +# +# Only the cookie is forwarded, and Traefik writes X-Forwarded-Method and +# X-Forwarded-Uri itself: were a client's own copies to reach the api, a GET +# sent with X-Forwarded-Method: OPTIONS would pass as a preflight. The auth +# call stays a GET whatever the request's method, since /streams/authorize +# only answers GET. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: mediamtx-auth + namespace: 5stack +spec: + forwardAuth: + address: http://api.5stack.svc.cluster.local:5585/streams/authorize + authRequestHeaders: [Cookie] + trustForwardHeader: false + preserveRequestMethod: false + maxResponseBodySize: 65536 +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: snapshot-cors + namespace: 5stack +spec: + headers: + accessControlAllowOriginList: ["*"] + accessControlAllowMethods: [GET, OPTIONS] + accessControlAllowHeaders: [Content-Type, Accept, If-None-Match] +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: snapshot-rewrite + namespace: 5stack +spec: + replacePathRegex: + regex: ^/snapshot/(.+)$ + replacement: /game-streamer/$1/snapshot diff --git a/overlays/mediamtx/snapshot-ingress.yaml b/overlays/mediamtx/snapshot-ingress.yaml deleted file mode 100644 index 865df3e..0000000 --- a/overlays/mediamtx/snapshot-ingress.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: snapshot - namespace: 5stack - annotations: - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /game-streamer/$1/snapshot - nginx.ingress.kubernetes.io/proxy-body-size: "2m" - nginx.ingress.kubernetes.io/enable-cors: "true" - nginx.ingress.kubernetes.io/cors-allow-origin: "*" - nginx.ingress.kubernetes.io/cors-allow-methods: "GET, OPTIONS" - nginx.ingress.kubernetes.io/cors-allow-headers: "Content-Type, Accept, If-None-Match" -spec: - ingressClassName: nginx - rules: - - host: ${GAME_STREAM_DOMAIN} - http: - paths: - - path: /snapshot/(.+) - pathType: ImplementationSpecific - backend: - service: - name: api - port: - number: 5585 diff --git a/update.sh b/update.sh index 4d7025c..bc78ff9 100755 --- a/update.sh +++ b/update.sh @@ -45,10 +45,14 @@ if [ "$REVERSE_PROXY" != true ]; then ok "cert-manager is ready" fi -step "Building overlay manifests" -HTTP_REPLACEMENTS="$PANEL_DIR/overlays/http/http-replacements.yaml" -HTTPS_REPLACEMENTS="$PANEL_DIR/overlays/http/https-replacements.yaml" +# Before the overlay: its Gateway, routes and Middlewares need the CRDs, and +# on an upgrade from ingress-nginx Traefik has to be serving before it is +# handed ports 80/443. +step "Installing Traefik" +install_traefik || die "failed to install Traefik" +ok "Traefik is ready" +step "Building overlay manifests" # Whether an operator has asked for a TURN relay at all. Read straight from # coturn's own env file rather than the environment: this is the same file the # generated ConfigMap is built from, so the two can never disagree. @@ -56,48 +60,7 @@ TURN_DOMAIN="$(grep -s '^TURN_DOMAIN=' "$PANEL_DIR/overlays/coturn/coturn.env" | TURN_DOMAIN="${TURN_DOMAIN%\"}" TURN_DOMAIN="${TURN_DOMAIN#\"}" -OVERLAY_BASES=("vault" "local-secrets") -for BASE in "${OVERLAY_BASES[@]}"; do - for PROTOCOL in "http" "https"; do - OVERLAY="overlays/${BASE}-${PROTOCOL}" - mkdir -p "$OVERLAY" - # MediaMTX is a WebRTC relay, not an encoder, and it now also carries - # player cameras — which have nothing to do with GPU game streaming. - # Only the nvidia device plugin stays GPU-gated. - STREAMING_RESOURCES="$(if [[ "$PROTOCOL" == "https" ]]; then echo "- ../mediamtx-https"; else echo "- ../mediamtx"; fi)" - # The TURN relay is opt-in and most installs never need one: WebRTC - # connects directly for the large majority of players, and STUN covers - # most of the rest. Deployed only once TURN_DOMAIN is set, so nobody - # ends up running a relay -- on hostNetwork, holding port 3478 -- that - # they never asked for. It has no http/https split: it speaks TURN, not - # HTTP, and the media it relays is already DTLS-encrypted end to end. - if [ -n "$TURN_DOMAIN" ]; then - STREAMING_RESOURCES="$STREAMING_RESOURCES -- ../coturn" - fi - if [ "$GPU_VENDOR" = "nvidia" ]; then - STREAMING_RESOURCES="- ../nvidia -$STREAMING_RESOURCES" - fi - - cat > "$OVERLAY/kustomization.yaml" <> "$OVERLAY/kustomization.yaml" - else - cat "$HTTP_REPLACEMENTS" >> "$OVERLAY/kustomization.yaml" - fi - done -done +write_overlays ok "overlays generated" step "Applying kustomize overlay" @@ -146,6 +109,8 @@ if [ "$REVERSE_PROXY" != true ]; then disown_shim_owned_certificates fi +retire_ingress_nginx || die "Traefik did not take over ports 80/443" + if [ "$VAULT_MANAGER" = true ]; then step "Syncing Vault secrets" if ! resync_vault_secrets; then diff --git a/utils/apply_overlay.sh b/utils/apply_overlay.sh index 72fe2bb..2c9ff35 100644 --- a/utils/apply_overlay.sh +++ b/utils/apply_overlay.sh @@ -8,8 +8,8 @@ # Is this failure "an admission webhook isn't serving yet" rather than "the # manifest is wrong"? Worth telling apart: the first is worth waiting out, the # second should be reported immediately instead of after a minute of pointless -# retries. Both cert-manager and ingress-nginx front their resources with a -# failurePolicy: Fail webhook, and both produce these strings while starting. +# retries. cert-manager fronts its resources with a failurePolicy: Fail +# webhook, and produces these strings while starting. webhook_unavailable_error() { case "$1" in *"failed calling webhook"*|\ diff --git a/utils/install_ingress_nginx.sh b/utils/install_ingress_nginx.sh deleted file mode 100755 index 4ff118c..0000000 --- a/utils/install_ingress_nginx.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash - -INGRESS_NGINX_MANIFEST="https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.9/deploy/static/provider/baremetal/deploy.yaml" - -# Exit codes are load bearing: install.sh treats a failed install (1) as fatal -# and a webhook that is merely slow (2) as a warning, because the first means -# there is no ingress layer at all and the second usually resolves during the -# apply that follows. -install_ingress_nginx() { - local quiet=${1:-false} - local timeout="${2:-600}" - local deadline=$((SECONDS + timeout)) - # Was bare `kubectl` here and `--kubeconfig` in the webhook wait, two calls - # in the same function reaching the cluster by different routes. Same array - # pattern as apply_overlay; empty when KUBECONFIG is unset. - local -a kc=() - [ -n "$KUBECONFIG" ] && kc=(--kubeconfig="$KUBECONFIG") - - echo "Installing Ingress Nginx..." - - # The status of this apply used to be discarded by output_redirect, so a - # missing cluster or a GitHub outage fell straight through to the wait loop - # below -- which then polled forever for pods that were never created. - if [ "$quiet" = true ] && type output_redirect &> /dev/null; then - if ! output_redirect kubectl "${kc[@]}" apply -f "$INGRESS_NGINX_MANIFEST"; then - echo "Failed to apply the Ingress Nginx manifest from $INGRESS_NGINX_MANIFEST" >&2 - return 1 - fi - else - if ! kubectl "${kc[@]}" apply -f "$INGRESS_NGINX_MANIFEST"; then - echo "Failed to apply the Ingress Nginx manifest from $INGRESS_NGINX_MANIFEST" >&2 - return 1 - fi - fi - - echo "Waiting for Ingress Nginx to be ready..." - while true; do - PODS=$(kubectl "${kc[@]}" get pods -n ingress-nginx -l app.kubernetes.io/component=controller -o jsonpath='{.items[*].metadata.name}' 2>/dev/null) - if [[ -n "$PODS" ]]; then - if kubectl "${kc[@]}" wait --namespace ingress-nginx \ - --for=condition=Ready pod \ - --selector=app.kubernetes.io/component=controller \ - --timeout=60s 2>/dev/null; then - echo "Ingress Nginx is ready!" - break - fi - fi - - # Bounded, unlike the original. An unbounded wait here meant install.sh - # hung silently instead of ever reaching the error handling below it. - if [ "$SECONDS" -ge "$deadline" ]; then - echo "Ingress Nginx controller did not become ready after ${timeout}s:" >&2 - kubectl "${kc[@]}" get pods -n ingress-nginx >&2 - return 1 - fi - - sleep 5 - done - - wait_for_ingress_admission || return 2 -} - -# A Ready controller is not the same thing as a usable cluster. ingress-nginx -# installs a ValidatingWebhookConfiguration with failurePolicy: Fail, and its -# caBundle is filled in by a separate `ingress-nginx-admission-patch` Job that -# can still be running once the controller reports Ready. In that window every -# Ingress creation is rejected -- "x509: certificate signed by unknown -# authority" -- which silently drops the entire ingress layer out of an apply -# that otherwise looks like it worked. -wait_for_ingress_admission() { - local timeout="${1:-180}" - local probe - - probe="$(cat <<'YAML' -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ingress-nginx-admission-probe - namespace: default -spec: - ingressClassName: nginx - rules: - - host: admission-probe.5stack.invalid - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: admission-probe - port: - number: 80 -YAML -)" - - wait_for_webhook "Ingress Nginx admission webhook" "$probe" "$timeout" -} diff --git a/utils/install_traefik.sh b/utils/install_traefik.sh new file mode 100644 index 0000000..3cce98b --- /dev/null +++ b/utils/install_traefik.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +# Everything that installs Traefik lives here and in charts/, as two releases +# applied by k3s's helm-controller: the CRDs, then Traefik with the shared +# Gateway. Two, because a release whose templates define the Gateway API kinds +# cannot also create a GatewayClass or Gateway: Helm resolves every kind before +# it applies anything. The panel's own Gateway and routes are in the panel +# overlay and only need this to have run first. +install_traefik() { + local ports_env="${1:-$PANEL_DIR/overlays/config/traefik-config.env}" + local minor http_port https_port crd + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + # Gateway API v1.6's CRDs validate with CEL's isIP(), which the API server + # only has from Kubernetes 1.31; older servers reject the CRDs outright. + minor="$(kubectl "${kc[@]}" version -o json 2>/dev/null | sed -n 's/.*"minor": *"\([0-9]*\).*/\1/p' | tail -n 1)" + if [ -n "$minor" ] && [ "$minor" -lt 31 ]; then + err "Kubernetes 1.$minor is too old for the Gateway API; upgrade k3s to 1.31 or newer first" + return 1 + fi + + http_port="$(grep -s '^HTTP_PORT=' "$ports_env" | tail -n 1 | cut -d= -f2- | tr -d '\r" ')" + https_port="$(grep -s '^HTTPS_PORT=' "$ports_env" | tail -n 1 | cut -d= -f2- | tr -d '\r" ')" + http_port="${http_port:-80}" + https_port="${https_port:-443}" + if ! [[ "$http_port" =~ ^[0-9]+$ && "$https_port" =~ ^[0-9]+$ ]]; then + err "HTTP_PORT and HTTPS_PORT in $ports_env must be port numbers" + return 1 + fi + + install_helm_chart gateway-crds "$PANEL_DIR/charts/gateway-crds" "" || return 1 + + for crd in gatewayclasses.gateway.networking.k8s.io gateways.gateway.networking.k8s.io \ + httproutes.gateway.networking.k8s.io middlewares.traefik.io; do + if ! output_redirect kubectl "${kc[@]}" wait --for=condition=Established "crd/$crd" --timeout=120s; then + err "CRD $crd never became established" + return 1 + fi + done + + # The Service is externalTrafficPolicy: Local, so only the node running + # Traefik answers on these ports -- and DNS points at the server node, not + # at a game node that joined later. + install_helm_chart gateway "$PANEL_DIR/charts/gateway" "$(cat <&2 + return 1 + fi +} + +# A chart directory as the .tgz Helm expects, byte-identical between runs where +# GNU tar allows it, so an unchanged chart is not reinstalled. +chart_archive() { + local parent name + parent="$(dirname "$1")" + name="$(basename "$1")" + if tar --version 2>/dev/null | grep -q GNU; then + tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner --exclude='.*' -cf - -C "$parent" "$name" | gzip -n + else + tar --exclude='.*' -cf - -C "$parent" "$name" | gzip -n + fi +} + +# Applies a chart from this checkout as a k3s HelmChart (no registry, no helm +# binary) and waits on the helm-install Job that helm-controller runs for it. +# That Job keeps its name across changes; a changed HelmChart gets a new Job, +# so a Job with the old UID is the previous config's and is not waited on. +install_helm_chart() { + local name="$1" dir="$2" values="$3" + local manifest old_generation new_generation old_uid uid complete failed deadline + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + # A k3s that has just started registers its HelmChart CRD a few seconds in. + deadline=$((SECONDS + 120)) + until kubectl "${kc[@]}" get crd helmcharts.helm.cattle.io >/dev/null 2>&1; do + if [ "$SECONDS" -ge "$deadline" ]; then + err "this cluster has no HelmChart CRD; Traefik is installed through k3s's helm-controller" + return 1 + fi + sleep 2 + done + + manifest="$(mktemp)" + trap 'rm -f "$manifest"' RETURN + + { + printf 'apiVersion: helm.cattle.io/v1\nkind: HelmChart\nmetadata:\n name: %s\n namespace: kube-system\n' "$name" + printf 'spec:\n targetNamespace: traefik\n createNamespace: true\n' + # abort, not the default reinstall: a failed upgrade must leave the + # running Traefik alone rather than uninstall it. + printf ' failurePolicy: abort\n backOffLimit: 3\n' + printf ' chartContent: %s\n' "$(chart_archive "$dir" | base64 | tr -d '\n')" + if [ -n "$values" ]; then + printf ' valuesContent: |\n' + printf '%s\n' "$values" | sed 's/^/ /' + fi + } >"$manifest" + + old_generation="$(kubectl "${kc[@]}" get helmchart "$name" -n kube-system -o jsonpath='{.metadata.generation}' 2>/dev/null)" + old_uid="$(kubectl "${kc[@]}" get job "helm-install-$name" -n kube-system -o jsonpath='{.metadata.uid}' 2>/dev/null)" + + # Server-side: chartContent is far past the 256KiB a client-side apply can + # keep in its last-applied annotation. + if ! output_redirect kubectl "${kc[@]}" apply --server-side --force-conflicts --field-manager=5stack-panel -f "$manifest"; then + err "could not apply HelmChart $name (k3s's helm-controller is required)" + return 1 + fi + new_generation="$(kubectl "${kc[@]}" get helmchart "$name" -n kube-system -o jsonpath='{.metadata.generation}')" + if [ "$new_generation" = "$old_generation" ]; then + old_uid="" + fi + + echo "Waiting for helm-controller to apply $name..." + deadline=$((SECONDS + 600)) + while true; do + IFS=$'\t' read -r uid complete failed < <(kubectl "${kc[@]}" get job "helm-install-$name" -n kube-system \ + -o jsonpath='{.metadata.uid}{"\t"}{.status.conditions[?(@.type=="Complete")].status}{"\t"}{.status.conditions[?(@.type=="Failed")].status}{"\n"}' 2>/dev/null) + if [ -n "$uid" ] && [ "$uid" != "$old_uid" ]; then + if [ "$complete" = "True" ]; then + return 0 + fi + if [ "$failed" = "True" ]; then + err "helm-controller could not apply $name:" + kubectl "${kc[@]}" logs "job/helm-install-$name" -n kube-system --tail=30 >&2 + return 1 + fi + fi + if [ "$SECONDS" -ge "$deadline" ]; then + err "helm-controller did not apply $name in time:" + kubectl "${kc[@]}" logs "job/helm-install-$name" -n kube-system --tail=30 >&2 + return 1 + fi + sleep 3 + done +} diff --git a/utils/retire_ingress_nginx.sh b/utils/retire_ingress_nginx.sh new file mode 100644 index 0000000..fc15404 --- /dev/null +++ b/utils/retire_ingress_nginx.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# The Ingresses the panel created before it moved to the Gateway API. Named +# rather than selected: plugins keep their own nginx-class Ingresses in the +# same namespace, and Traefik serves those. +PANEL_INGRESSES=(api demos ws relay file-manager hasura web s3-console typesense mediamtx snapshot hasura-dashboard queues) + +# Hands ports 80/443 from ingress-nginx to Traefik, then removes ingress-nginx. +# Safe to re-run, and a no-op on an install that never had it. +# +# k3s ServiceLB gives a host port to one LoadBalancer Service only, so +# Traefik's Service sits pending until ingress-nginx's is deleted; traffic is +# down for the seconds in between. The old Ingresses go only after that: until +# then ingress-nginx is what serves them, and Traefik's own copies of them +# route to the same backends as the HTTPRoutes. +retire_ingress_nginx() { + local deadline + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + # First: its failurePolicy is Fail, so once the controller behind it is gone + # it rejects every Ingress write in the cluster, cert-manager's included. + output_redirect kubectl "${kc[@]}" delete validatingwebhookconfiguration ingress-nginx-admission --ignore-not-found + + if kubectl "${kc[@]}" get service ingress-nginx-controller -n ingress-nginx >/dev/null 2>&1; then + echo "Moving ports 80/443 from ingress-nginx to Traefik..." + output_redirect kubectl "${kc[@]}" delete service ingress-nginx-controller -n ingress-nginx --wait=true + fi + + deadline=$((SECONDS + 180)) + until [ -n "$(kubectl "${kc[@]}" get service traefik -n traefik -o jsonpath='{.status.loadBalancer.ingress}' 2>/dev/null)" ]; do + if [ "$SECONDS" -ge "$deadline" ]; then + err "Traefik's LoadBalancer Service never got an address:" + kubectl "${kc[@]}" get service traefik -n traefik >&2 + kubectl "${kc[@]}" get pods -A -l svccontroller.k3s.cattle.io/svcname=traefik >&2 + return 1 + fi + sleep 2 + done + + # Orphaned, not cascaded: on an old install ingress-shim may still own the + # 5stack-ssl Certificate through one of these. + output_redirect kubectl "${kc[@]}" delete ingress -n 5stack --ignore-not-found --cascade=orphan "${PANEL_INGRESSES[@]}" + output_redirect kubectl "${kc[@]}" delete namespace ingress-nginx --ignore-not-found --wait=false + output_redirect kubectl "${kc[@]}" delete clusterrole,clusterrolebinding ingress-nginx ingress-nginx-admission --ignore-not-found +} diff --git a/utils/setup-env.sh b/utils/setup-env.sh index 5cc6a6c..bb0fe5c 100755 --- a/utils/setup-env.sh +++ b/utils/setup-env.sh @@ -38,7 +38,7 @@ if [ -z "$KUBECONFIG" ]; then KUBECONFIG="/etc/rancher/k3s/k3s.yaml" fi # Exported so the calls that don't pass --kubeconfig explicitly (install.sh, -# install_ingress_nginx) talk to the same cluster as the ones that do. +# install_traefik) talk to the same cluster as the ones that do. export KUBECONFIG setup_kustomize @@ -226,6 +226,11 @@ if [ -d "base/properties" ]; then rm -rf base/properties fi +# The ports file was nginx-config.env while ingress-nginx served the panel. +if [ -f overlays/config/nginx-config.env ] && [ ! -f overlays/config/traefik-config.env ]; then + mv overlays/config/nginx-config.env overlays/config/traefik-config.env +fi + copy_config_or_secrets "overlays/local-secrets" "overlays/local-secrets" copy_config_or_secrets "overlays/config" "overlays/config" copy_config_or_secrets "overlays/mediamtx" "overlays/mediamtx" diff --git a/utils/utils.sh b/utils/utils.sh index 4488f07..da8d410 100755 --- a/utils/utils.sh +++ b/utils/utils.sh @@ -11,6 +11,7 @@ source "$SCRIPT_DIR/colors.sh" source "$SCRIPT_DIR/check_sudo.sh" source "$SCRIPT_DIR/output_redirect.sh" source "$SCRIPT_DIR/apply_overlay.sh" +source "$SCRIPT_DIR/write_overlays.sh" source "$SCRIPT_DIR/cert_manager.sh" source "$SCRIPT_DIR/update_env_var.sh" source "$SCRIPT_DIR/copy_config_or_secrets.sh" @@ -26,7 +27,8 @@ source "$SCRIPT_DIR/setup_postgres_connection_string.sh" source "$SCRIPT_DIR/setup_steam_web_api_key.sh" source "$SCRIPT_DIR/setup_fixtures.sh" source "$SCRIPT_DIR/setup_k3s_token.sh" -source "$SCRIPT_DIR/install_ingress_nginx.sh" +source "$SCRIPT_DIR/install_traefik.sh" +source "$SCRIPT_DIR/retire_ingress_nginx.sh" source "$SCRIPT_DIR/choose_k8s_context.sh" source "$SCRIPT_DIR/checkout_repos.sh" source "$SCRIPT_DIR/check_dev_dependencies.sh" diff --git a/utils/write_overlays.sh b/utils/write_overlays.sh new file mode 100644 index 0000000..b8eb3a0 --- /dev/null +++ b/utils/write_overlays.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Writes overlays/{vault,local-secrets}-{http,https}, of which update.sh applies +# the one REVERSE_PROXY and VAULT_MANAGER select. Reads TURN_DOMAIN and +# GPU_VENDOR. +write_overlays() { + local BASE PROTOCOL OVERLAY STREAMING_RESOURCES + for BASE in vault local-secrets; do + for PROTOCOL in "http" "https"; do + OVERLAY="$PANEL_DIR/overlays/${BASE}-${PROTOCOL}" + mkdir -p "$OVERLAY" + # MediaMTX is a WebRTC relay, not an encoder, and it now also carries + # player cameras — which have nothing to do with GPU game streaming. + # Only the nvidia device plugin stays GPU-gated. + STREAMING_RESOURCES="$(if [[ "$PROTOCOL" == "https" ]]; then echo "- ../mediamtx-https"; else echo "- ../mediamtx"; fi)" + # The TURN relay is opt-in and most installs never need one: WebRTC + # connects directly for the large majority of players, and STUN covers + # most of the rest. Deployed only once TURN_DOMAIN is set, so nobody + # ends up running a relay -- on hostNetwork, holding port 3478 -- that + # they never asked for. It has no http/https split: it speaks TURN, not + # HTTP, and the media it relays is already DTLS-encrypted end to end. + if [ -n "$TURN_DOMAIN" ]; then + STREAMING_RESOURCES="$STREAMING_RESOURCES +- ../coturn" + fi + if [ "$GPU_VENDOR" = "nvidia" ]; then + STREAMING_RESOURCES="- ../nvidia +$STREAMING_RESOURCES" + fi + + cat > "$OVERLAY/kustomization.yaml" <> "$OVERLAY/kustomization.yaml" + else + cat "$PANEL_DIR/overlays/http/http-replacements.yaml" >> "$OVERLAY/kustomization.yaml" + fi + done + done +} From 8ffe3324638374467c16cbf338734fde48af4b3f Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 28 Oct 2026 13:30:16 -0400 Subject: [PATCH 02/17] test: k3s-in-Docker tests for the Gateway API routing layer, from a fresh install and from ingress-nginx, and a workflow publishing the gateway charts to ghcr.io --- .github/workflows/gateway-chart.yml | 33 + .github/workflows/gateway.yml | 61 ++ tests/gateway/backend.py | 207 +++++++ tests/gateway/client.py | 122 ++++ .../gateway/fixtures/ingress-nginx-panel.yaml | 579 ++++++++++++++++++ tests/gateway/fresh_install_test.sh | 403 ++++++++++++ tests/gateway/lib.sh | 269 ++++++++ tests/gateway/upgrade_test.sh | 221 +++++++ tests/scripts/setup_env_test.sh | 10 + update.sh | 2 + utils/install_traefik.sh | 7 +- utils/retire_ingress_nginx.sh | 7 + 12 files changed, 1918 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/gateway-chart.yml create mode 100644 .github/workflows/gateway.yml create mode 100644 tests/gateway/backend.py create mode 100644 tests/gateway/client.py create mode 100644 tests/gateway/fixtures/ingress-nginx-panel.yaml create mode 100644 tests/gateway/fresh_install_test.sh create mode 100644 tests/gateway/lib.sh create mode 100644 tests/gateway/upgrade_test.sh diff --git a/.github/workflows/gateway-chart.yml b/.github/workflows/gateway-chart.yml new file mode 100644 index 0000000..dad2953 --- /dev/null +++ b/.github/workflows/gateway-chart.yml @@ -0,0 +1,33 @@ +name: Gateway chart + +# Publishes charts/gateway-crds and charts/gateway as OCI charts at +# ghcr.io/5stackgg/charts/, tagged with the version in their Chart.yaml. +on: + push: + branches: [main] + paths: + - "charts/gateway/**" + - "charts/gateway-crds/**" + - .github/workflows/gateway-chart.yml + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin + - run: | + helm package charts/gateway-crds --destination dist + helm package charts/gateway --destination dist + for chart in dist/*.tgz; do + helm push "$chart" oci://ghcr.io/5stackgg/charts + done diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml new file mode 100644 index 0000000..159001f --- /dev/null +++ b/.github/workflows/gateway.yml @@ -0,0 +1,61 @@ +name: Gateway + +on: + push: + branches: [main] + paths: + - update.sh + - install.sh + - dev.sh + - utils/apply_overlay.sh + - utils/install_traefik.sh + - utils/retire_ingress_nginx.sh + - utils/write_overlays.sh + - "charts/**" + - "base/gateway/**" + - "base/*/httproute*.yaml" + - "overlays/http/**" + - "overlays/mediamtx/**" + - "overlays/mediamtx-https/**" + - "overlays/cert-manager/**" + - "overlays/config/*.env.example" + - "tests/gateway/**" + - .github/workflows/gateway.yml + pull_request: + paths: + - update.sh + - install.sh + - dev.sh + - utils/apply_overlay.sh + - utils/install_traefik.sh + - utils/retire_ingress_nginx.sh + - utils/write_overlays.sh + - "charts/**" + - "base/gateway/**" + - "base/*/httproute*.yaml" + - "overlays/http/**" + - "overlays/mediamtx/**" + - "overlays/mediamtx-https/**" + - "overlays/cert-manager/**" + - "overlays/config/*.env.example" + - "tests/gateway/**" + - .github/workflows/gateway.yml + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: shellcheck -x utils/install_traefik.sh utils/retire_ingress_nginx.sh utils/write_overlays.sh tests/gateway/*.sh + + fresh-install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: bash tests/gateway/fresh_install_test.sh + + upgrade-from-ingress-nginx: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: bash tests/gateway/upgrade_test.sh diff --git a/tests/gateway/backend.py b/tests/gateway/backend.py new file mode 100644 index 0000000..d0d0c2e --- /dev/null +++ b/tests/gateway/backend.py @@ -0,0 +1,207 @@ +"""Stands in for every Service the gateway routes to. + +One process listens on each port in PORTS ("5585:api,8080:hasura,..."), so a +Service named like the real one reaches it on the real port. Every response is +JSON describing the request as it arrived, which is what the tests assert on. + +/streams/authorize, /plugins/authorize and /qx/authorize answer like the api's +forward-auth on branch feature/forward-auth-traefik: X-Original-* and +X-Forwarded-* that disagree are refused, a forwarded OPTIONS is a preflight, +and otherwise the session cookie decides. +""" + +import base64 +import hashlib +import json +import os +import struct +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import parse_qs, urlsplit + +AUTH_LOG = [] +AUTH_LOCK = threading.Lock() +WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + +def recv_exact(sock, n): + data = b"" + while len(data) < n: + chunk = sock.recv(n - len(data)) + if not chunk: + raise ConnectionError("closed") + data += chunk + return data + + +def ws_frame(opcode, payload): + header = bytes([0x80 | opcode]) + if len(payload) < 126: + header += bytes([len(payload)]) + elif len(payload) < 65536: + header += bytes([126]) + struct.pack(">H", len(payload)) + else: + header += bytes([127]) + struct.pack(">Q", len(payload)) + return header + payload + + +def handler_for(name): + class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *args): + pass + + def headers_dict(self): + return {k.lower(): v for k, v in self.headers.items()} + + def read_body(self): + total = 0 + if self.headers.get("Transfer-Encoding", "").lower() == "chunked": + while True: + size = int(self.rfile.readline().split(b";")[0].strip() or b"0", 16) + if size == 0: + self.rfile.readline() + break + remaining = size + while remaining: + remaining -= len(self.rfile.read(min(remaining, 1 << 20))) + self.rfile.readline() + total += size + return total + remaining = int(self.headers.get("Content-Length") or 0) + while remaining: + got = len(self.rfile.read(min(remaining, 1 << 20))) + if not got: + break + remaining -= got + total += got + return total + + def reply(self, status, body=None, headers=None): + data = b"" if body is None else json.dumps(body, sort_keys=True).encode() + self.send_response(status) + for k, v in (headers or {}).items(): + self.send_header(k, v) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def describe(self, body_bytes): + return { + "backend": name, + "method": self.command, + "path": self.path, + "host": self.headers.get("Host"), + "headers": self.headers_dict(), + "body_bytes": body_bytes, + } + + def authorize(self): + h = self.headers_dict() + with AUTH_LOCK: + AUTH_LOG.append({"method": self.command, "path": self.path, "headers": h}) + for nginx, traefik in (("x-original-method", "x-forwarded-method"), ("x-original-url", "x-forwarded-uri")): + a, b = h.get(nginx), h.get(traefik) + if a is not None and b is not None: + if nginx == "x-original-method" and a.upper() != b.upper(): + return self.reply(401) + if nginx == "x-original-url" and urlsplit(a).path != urlsplit(b).path: + return self.reply(401) + method = (h.get("x-original-method") or h.get("x-forwarded-method") or self.command).upper() + if method == "OPTIONS": + return self.reply(200) + cookie = h.get("cookie", "") + role = "administrator" if "session=admin" in cookie else "user" if "session=ok" in cookie else None + if role is None: + return self.reply(401) + if h.get("x-5stack-required-role") == "administrator" and role != "administrator": + return self.reply(403) + return self.reply(200, headers={"X-5stack-Role": role}) + + def websocket(self): + key = self.headers["Sec-WebSocket-Key"] + accept = base64.b64encode(hashlib.sha1((key + WS_GUID).encode()).digest()).decode() + self.send_response(101) + self.send_header("Upgrade", "websocket") + self.send_header("Connection", "Upgrade") + self.send_header("Sec-WebSocket-Accept", accept) + self.end_headers() + self.wfile.flush() + sock = self.connection + sock.settimeout(None) + hello = json.dumps({"backend": name, "path": self.path, "host": self.headers.get("Host")}).encode() + sock.sendall(ws_frame(0x1, hello)) + try: + while True: + b1, b2 = recv_exact(sock, 2) + opcode, length = b1 & 0x0F, b2 & 0x7F + if length == 126: + length = struct.unpack(">H", recv_exact(sock, 2))[0] + elif length == 127: + length = struct.unpack(">Q", recv_exact(sock, 8))[0] + mask = recv_exact(sock, 4) if b2 & 0x80 else b"\0\0\0\0" + payload = bytes(c ^ mask[i % 4] for i, c in enumerate(recv_exact(sock, length))) + if opcode == 0x8: + sock.sendall(ws_frame(0x8, b"")) + break + sock.sendall(ws_frame(0xA if opcode == 0x9 else opcode, payload)) + except (ConnectionError, OSError): + pass + self.close_connection = True + + def stream(self, seconds): + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() + for i in range(seconds): + line = f"tick {i}\n".encode() + self.wfile.write(b"%x\r\n%s\r\n" % (len(line), line)) + self.wfile.flush() + time.sleep(1) + self.wfile.write(b"0\r\n\r\n") + self.wfile.flush() + + def handle_any(self): + path = urlsplit(self.path).path + if self.headers.get("Upgrade", "").lower() == "websocket": + return self.websocket() + if path.endswith(("/streams/authorize", "/plugins/authorize", "/qx/authorize")): + self.read_body() + return self.authorize() + if path.endswith("/__auth_log"): + self.read_body() + with AUTH_LOCK: + log = list(AUTH_LOG) + if "clear" in parse_qs(urlsplit(self.path).query): + AUTH_LOG.clear() + return self.reply(200, log) + if path.endswith("/stream"): + seconds = int(parse_qs(urlsplit(self.path).query).get("seconds", ["5"])[0]) + return self.stream(seconds) + body_bytes = self.read_body() + if self.command == "POST" and path.endswith("/whep"): + return self.reply(201, self.describe(body_bytes), {"Location": path + "/session-1"}) + return self.reply(200, self.describe(body_bytes)) + + do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = do_OPTIONS = do_HEAD = handle_any + + return Handler + + +def main(): + servers = [] + for entry in os.environ["PORTS"].split(","): + port, name = entry.split(":") + server = ThreadingHTTPServer(("0.0.0.0", int(port)), handler_for(name)) + server.daemon_threads = True + servers.append(server) + threading.Thread(target=server.serve_forever, daemon=True).start() + threading.Event().wait() + + +if __name__ == "__main__": + main() diff --git a/tests/gateway/client.py b/tests/gateway/client.py new file mode 100644 index 0000000..cb58360 --- /dev/null +++ b/tests/gateway/client.py @@ -0,0 +1,122 @@ +"""Runs in the client container, where curl can't drive a websocket or time +an outage. + + client.py ws IP HOST PATH HOLD_SECONDS + Opens wss://HOST/PATH against IP, echoes a frame through it every 10s + for HOLD_SECONDS, and prints what came back as JSON. + + client.py poll IP HOST PATH STOPFILE + Requests https://HOST/PATH against IP every 0.2s until STOPFILE exists, + and prints the requests that failed and the longest outage as JSON. +""" + +import base64 +import json +import os +import socket +import ssl +import struct +import sys +import time + + +def tls_socket(ip, host, timeout): + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + raw = socket.create_connection((ip, 443), timeout=timeout) + return ctx.wrap_socket(raw, server_hostname=host) + + +def read_frame(sock): + def exact(n): + data = b"" + while len(data) < n: + chunk = sock.recv(n - len(data)) + if not chunk: + raise ConnectionError("closed") + data += chunk + return data + + b1, b2 = exact(2) + length = b2 & 0x7F + if length == 126: + length = struct.unpack(">H", exact(2))[0] + elif length == 127: + length = struct.unpack(">Q", exact(8))[0] + return b1 & 0x0F, exact(length) + + +def send_frame(sock, payload): + mask = os.urandom(4) + masked = bytes(c ^ mask[i % 4] for i, c in enumerate(payload)) + sock.sendall(bytes([0x81, 0x80 | len(payload)]) + mask + masked) + + +def ws(ip, host, path, hold): + result = {"status": None, "hello": None, "echoes": 0, "held": 0, "error": None} + started = time.monotonic() + try: + sock = tls_socket(ip, host, 30) + key = base64.b64encode(os.urandom(16)).decode() + sock.sendall( + (f"GET {path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" + f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode() + ) + head = b"" + while b"\r\n\r\n" not in head: + head += sock.recv(1) + result["status"] = int(head.split(b" ")[1]) + _, hello = read_frame(sock) + result["hello"] = json.loads(hello) + sock.settimeout(30) + while True: + message = f"echo {result['echoes']}".encode() + send_frame(sock, message) + opcode, payload = read_frame(sock) + if opcode != 0x1 or payload != message: + raise ValueError(f"bad echo {opcode} {payload!r}") + result["echoes"] += 1 + if time.monotonic() - started >= hold: + break + time.sleep(10) + except Exception as e: + result["error"] = repr(e) + result["held"] = round(time.monotonic() - started) + print(json.dumps(result)) + + +def poll(ip, host, path, stopfile): + failures, outage_started, longest, requests = [], None, 0.0, 0 + started = time.monotonic() + while not os.path.exists(stopfile): + now = time.monotonic() + status = None + try: + sock = tls_socket(ip, host, 2) + sock.sendall(f"GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n".encode()) + status = int(sock.recv(64).split(b" ")[1]) + sock.close() + except Exception: + pass + requests += 1 + if status == 200: + if outage_started is not None: + longest = max(longest, now - outage_started) + outage_started = None + else: + failures.append({"t": round(now - started, 1), "status": status}) + if outage_started is None: + outage_started = now + time.sleep(0.2) + if outage_started is not None: + longest = max(longest, time.monotonic() - outage_started) + print(json.dumps({"requests": requests, "failed": len(failures), "longest_outage": round(longest, 1), + "failures": failures[:50]})) + + +if __name__ == "__main__": + if sys.argv[1] == "ws": + ws(sys.argv[2], sys.argv[3], sys.argv[4], int(sys.argv[5])) + else: + poll(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]) diff --git a/tests/gateway/fixtures/ingress-nginx-panel.yaml b/tests/gateway/fixtures/ingress-nginx-panel.yaml new file mode 100644 index 0000000..e26b860 --- /dev/null +++ b/tests/gateway/fixtures/ingress-nginx-panel.yaml @@ -0,0 +1,579 @@ +# The panel's ingress-nginx layer before the move to the Gateway API: every +# Ingress and the controller Service from 5stack-panel 2e4e8f3, rendered from +# overlays/local-secrets-https with the test domains. The upgrade test starts +# from this. +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.12.1 + name: ingress-nginx-controller + namespace: ingress-nginx +spec: + externalTrafficPolicy: Local + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - appProtocol: http + name: http + port: 80 + protocol: TCP + targetPort: http + - appProtocol: https + name: https + port: 443 + protocol: TCP + targetPort: https + selector: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + type: LoadBalancer +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/client-max-body-size: 4G + nginx.ingress.kubernetes.io/proxy-body-size: 4G + name: api + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: api.5stack.test + http: + paths: + - backend: + service: + name: api + port: + number: 5585 + path: /me + pathType: Exact + - backend: + service: + name: api + port: + number: 5585 + path: /matches + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /twitch + pathType: Prefix + - backend: + service: + name: hasura + port: + number: 8080 + path: /v1 + pathType: Prefix + - backend: + service: + name: hasura + port: + number: 8080 + path: /v1alpha1 + pathType: Prefix + - backend: + service: + name: hasura + port: + number: 8080 + path: /v2 + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /game-server-node/script + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /game-server-node/ping + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /avatars + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /trophies + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /system/healthz + pathType: Exact + - backend: + service: + name: api + port: + number: 5585 + path: /system/logs/download + pathType: Exact + - backend: + service: + name: api + port: + number: 5585 + path: /branding + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /news + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /utility + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /custom-pages + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /plugins + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /huds + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /events/media + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /snapshots + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /steam-match-history/upload + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /clip-views + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /voice + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /notifications + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /chat + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /sanctions/server + pathType: Prefix + - host: 5stack.test + http: + paths: + - backend: + service: + name: api + port: + number: 5585 + path: /auth + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /extension + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /telemetry + pathType: Exact + - backend: + service: + name: api + port: + number: 5585 + path: /discord-invite + pathType: Exact + - backend: + service: + name: api + port: + number: 5585 + path: /discord-bot + pathType: Exact + - backend: + service: + name: api + port: + number: 5585 + path: /calendar + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /quick-connect + pathType: Exact + tls: + - hosts: + - api.5stack.test + secretName: 5stack-ssl +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/chunked-transfer-encoding: "false" + nginx.ingress.kubernetes.io/client-max-body-size: "0" + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-request-buffering: "off" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + name: demos + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: demos.5stack.test + http: + paths: + - backend: + service: + name: api + port: + number: 5585 + path: /demos + pathType: Prefix + - backend: + service: + name: api + port: + number: 5585 + path: /clips + pathType: Prefix + - backend: + service: + name: rustfs + port: + number: 9000 + path: /5stack + pathType: Prefix + tls: + - hosts: + - demos.5stack.test + secretName: 5stack-ssl +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/client-max-body-size: 4G + nginx.ingress.kubernetes.io/proxy-body-size: 4G + name: file-manager + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: api.5stack.test + http: + paths: + - backend: + service: + name: api + port: + number: 5585 + path: /file-manager + pathType: Prefix + tls: + - hosts: + - api.5stack.test + secretName: 5stack-ssl +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /healthz + name: hasura + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: api.5stack.test + http: + paths: + - backend: + service: + name: hasura + port: + number: 8080 + path: /v2/healthz + pathType: Exact +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/auth-cache-duration: 200 30s + nginx.ingress.kubernetes.io/auth-cache-key: $http_cookie$mediamtx_path + nginx.ingress.kubernetes.io/auth-url: http://api.5stack.svc.cluster.local:5585/streams/authorize + nginx.ingress.kubernetes.io/cors-allow-credentials: "true" + nginx.ingress.kubernetes.io/cors-allow-headers: Content-Type, Accept, Authorization, + If-Match + nginx.ingress.kubernetes.io/cors-allow-methods: GET, POST, OPTIONS, PATCH, DELETE + nginx.ingress.kubernetes.io/cors-allow-origin: https://5stack.test + nginx.ingress.kubernetes.io/cors-expose-headers: Location + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-http-version: "1.1" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/use-regex: "true" + name: mediamtx + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: hls.5stack.test + http: + paths: + - backend: + service: + name: mediamtx + port: + number: 8889 + path: /(?[^/]+)/(whep|whip)(/[^/]+)?$ + pathType: ImplementationSpecific + - backend: + service: + name: mediamtx + port: + number: 8888 + path: /(?[^/]+)/([^/]+\.[^/]+)?$ + pathType: ImplementationSpecific + tls: + - hosts: + - hls.5stack.test + secretName: 5stack-mediamtx-ssl +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /match-relay/$1 + name: relay + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: tv.5stack.test + http: + paths: + - backend: + service: + name: api + port: + number: 5585 + path: /(.*) + pathType: ImplementationSpecific + tls: + - hosts: + - tv.5stack.test + secretName: 5stack-ssl +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: 2048m + nginx.ingress.kubernetes.io/rewrite-target: /$1 + name: s3-console + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: console.5stack.test + http: + paths: + - backend: + service: + name: rustfs + port: + number: 9001 + path: /(.*) + pathType: ImplementationSpecific + tls: + - hosts: + - console.5stack.test + secretName: 5stack-ssl +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/cors-allow-headers: Content-Type, Accept, If-None-Match + nginx.ingress.kubernetes.io/cors-allow-methods: GET, OPTIONS + nginx.ingress.kubernetes.io/cors-allow-origin: '*' + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/proxy-body-size: 2m + nginx.ingress.kubernetes.io/rewrite-target: /game-streamer/$1/snapshot + nginx.ingress.kubernetes.io/use-regex: "true" + name: snapshot + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: hls.5stack.test + http: + paths: + - backend: + service: + name: api + port: + number: 5585 + path: /snapshot/(.+) + pathType: ImplementationSpecific +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: typesense + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: search.5stack.test + http: + paths: + - backend: + service: + name: typesense + port: + number: 8108 + path: / + pathType: Prefix + tls: + - hosts: + - search.5stack.test + secretName: 5stack-ssl +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$1 + name: web + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: 5stack.test + http: + paths: + - backend: + service: + name: web + port: + number: 3000 + path: /(.*) + pathType: ImplementationSpecific + tls: + - hosts: + - 5stack.test + secretName: 5stack-ssl +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /ws/$1 + name: ws + namespace: 5stack +spec: + ingressClassName: nginx + rules: + - host: ws.5stack.test + http: + paths: + - backend: + service: + name: api + port: + number: 5585 + path: /(.*) + pathType: ImplementationSpecific + tls: + - hosts: + - ws.5stack.test + secretName: 5stack-ssl diff --git a/tests/gateway/fresh_install_test.sh b/tests/gateway/fresh_install_test.sh new file mode 100644 index 0000000..031918b --- /dev/null +++ b/tests/gateway/fresh_install_test.sh @@ -0,0 +1,403 @@ +#!/bin/bash + +# A fresh install's routing layer: Traefik from charts/ through k3s's +# helm-controller (install_traefik), then the Gateway, HTTPRoutes and +# Middlewares of the overlay update.sh builds, in front of stub backends. +# https mode first -- before and after its certificates exist -- then http. + +# shellcheck source=tests/gateway/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +trap cleanup EXIT + +API="api.$DOMAIN" +WEB="$DOMAIN" +DEMOS="demos.$DOMAIN" +WS="ws.$DOMAIN" +RELAY="tv.$DOMAIN" +HLS="hls.$DOMAIN" +SEARCH="search.$DOMAIN" +CONSOLE="console.$DOMAIN" +ORIGIN="https://$DOMAIN" + +# What cert-manager creates from overlays/cert-manager/issuer.yaml for a +# challenge: the Issuer's class and ingressTemplate annotations on an Ingress +# for /.well-known/acme-challenge/. +solver_ingress() { + python3 -I - "$WORK_DIR/issuer.yaml" "$@" <<'PY' +import re, sys +issuer = open(sys.argv[1]).read() +klass = re.search(r"ingressClassName: (\S+)", issuer).group(1) +annotations = re.findall(r"(?m)^ +(traefik\.ingress\.kubernetes\.io/\S+): (.+)$", issuer) +print(f"""apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cm-acme-http-solver-test + namespace: 5stack + annotations:""") +for key, value in annotations: + print(f" {key}: {value}") +print(f"""spec: + ingressClassName: {klass} + rules:""") +for host in sys.argv[2:]: + print(f""" - host: {host} + http: + paths: + - path: /.well-known/acme-challenge/test-token + pathType: ImplementationSpecific + backend: + service: + name: acme-solver + port: + number: 8089""") +PY +} + +test_http01_is_answered_beside_the_https_redirect() { + wait_for 200 http "$API" /.well-known/acme-challenge/test-token + expect_json .backend acme-solver + call http "$HLS" /.well-known/acme-challenge/test-token + expect_code 200 + expect_json .path /.well-known/acme-challenge/test-token + call http "$API" /me + expect_code 308 + expect_header Location "https://$API/me" +} + +test_hosts_serve_while_the_stream_certificate_is_missing() { + wait_for 200 https "$API" /me + expect_json .backend api + call https "$WEB" / + expect_code 200 + expect_json .backend web +} + +test_api_allowlist_paths_reach_their_backends() { + local entry path backend + for entry in /me:api /system/healthz:api /system/logs/download:api /matches/1/live:api /twitch/x:api \ + /game-server-node/script:api /game-server-node/ping/1:api /avatars/1.png:api /trophies/x:api \ + /branding/logo:api /news/1:api /utility/x:api /custom-pages/x:api /plugins/authorize/x:api \ + /huds/x.zip:api /events/media/1:api /snapshots/1:api /steam-match-history/upload:api /clip-views/1:api \ + /voice/1:api /notifications/1:api /chat/1:api /sanctions/server/1:api /file-manager/upload:api \ + /v1/graphql:hasura /v1alpha1/x:hasura /v2/query:hasura; do + path="${entry%:*}" + backend="${entry##*:}" + call https "$API" "$path" + expect_code 200 + expect_json .backend "$backend" + expect_json .path "$path" + done +} + +test_paths_outside_the_allowlist_are_404() { + local path + for path in / /nope /matchesx /me/x /system/healthz/x /system/logs /v3 /ws /auth/steam; do + call https "$API" "$path" + expect_code 404 + done +} + +test_hasura_healthz_is_rewritten() { + call https "$API" /v2/healthz + expect_code 200 + expect_json .backend hasura + expect_json .path /healthz +} + +test_web_host_sends_its_api_paths_to_the_api() { + local entry path backend + for entry in /auth/steam:api /extension/x:api /telemetry:api /discord-invite:api /discord-bot:api \ + /calendar/x.ics:api /quick-connect:api /telemetry/x:web /authx:web /:web "/matches/1?tab=x:web"; do + path="${entry%:*}" + backend="${entry##*:}" + call https "$WEB" "$path" + expect_code 200 + expect_json .backend "$backend" + expect_json .path "$path" + done +} + +test_demos_reach_rustfs_with_host_and_path_as_signed() { + local path='/5stack/demos/a%20b%2Bc.dem?X-Amz-Signature=ab%2Fcd&partNumber=3' + call https "$DEMOS" "$path" + expect_code 200 + expect_json .backend rustfs + expect_json .path "$path" + expect_json .host "$DEMOS" + call https "$DEMOS" /demos/1/download + expect_json .backend api + call https "$DEMOS" /clips/1 + expect_json .backend api + call https "$DEMOS" /other + expect_code 404 +} + +test_a_64mib_part_put_reaches_rustfs_whole() { + client sh -c 'head -c 67108864 /dev/zero >/tmp/part' + call https "$DEMOS" '/5stack/demos/x.dem?partNumber=1&uploadId=u' -X PUT --data-binary @/tmp/part + expect_code 200 + expect_json .method PUT + expect_json .body_bytes 67108864 +} + +test_ws_and_relay_hosts_rewrite_into_the_api() { + call https "$WS" '/socket?EIO=4' + expect_json .backend api + expect_json .path '/ws/socket?EIO=4' + call https "$WS" / + expect_json .path /ws/ + call https "$RELAY" '/match/1/0/start?x=1' + expect_json .path '/match-relay/match/1/0/start?x=1' +} + +test_websocket_upgrades_through_the_ws_host() { + local out + out="$(client python3 /client.py ws "$NODE_IP" "$WS" /live 0)" + [ "$(jq -r .status <<<"$out")" = 101 ] || fail "no 101: $out" + [ "$(jq -r .hello.path <<<"$out")" = /ws/live ] || fail "wrong upstream path: $out" + [ "$(jq -r .echoes <<<"$out")" -ge 1 ] || fail "no echo: $out" +} + +test_typesense_and_s3_console_hosts() { + call https "$SEARCH" /collections + expect_json .backend typesense + call https "$CONSOLE" /browser + expect_json .backend rustfs-console + expect_json .path /browser +} + +test_mediamtx_preflight_is_answered_before_forward_auth() { + auth_log '?clear' >/dev/null + call https "$HLS" /m1/whep -X OPTIONS -H "Origin: $ORIGIN" -H 'Access-Control-Request-Method: POST' \ + -H 'Access-Control-Request-Headers: content-type' + expect_code 200 + expect_header Access-Control-Allow-Origin "$ORIGIN" + expect_header Access-Control-Allow-Credentials true + [ -z "$BODY" ] || fail "the preflight reached a backend: $BODY" + [ "$(auth_log | jq length)" = 0 ] || fail "the preflight went through forward-auth" +} + +test_mediamtx_requires_a_session() { + call https "$HLS" /m1/index.m3u8 -H "Origin: $ORIGIN" + expect_code 401 + expect_header Access-Control-Allow-Origin "$ORIGIN" + call https "$HLS" /m1/index.m3u8 -H "Origin: $ORIGIN" -H 'Cookie: session=ok' + expect_code 200 + expect_json .backend mediamtx-hls + expect_json .path /m1/index.m3u8 + call https "$HLS" /m1/ -H 'Cookie: session=ok' + expect_json .backend mediamtx-hls +} + +test_whep_post_and_patch_pass_forward_auth() { + auth_log '?clear' >/dev/null + call https "$HLS" /m1/whep -X POST -H "Origin: $ORIGIN" -H 'Cookie: session=ok' \ + -H 'Content-Type: application/sdp' --data-binary 'v=0' + expect_code 201 + expect_json .backend mediamtx-whip + expect_header Location /m1/whep/session-1 + expect_header Access-Control-Expose-Headers Location + local entry + entry="$(auth_log | jq -c '.[-1]')" + [ "$(jq -r .method <<<"$entry")" = GET ] || fail "the auth call was not a GET: $entry" + [ "$(jq -r '.headers["x-forwarded-method"]' <<<"$entry")" = POST ] || fail "$entry" + [ "$(jq -r '.headers["x-forwarded-uri"]' <<<"$entry")" = /m1/whep ] || fail "$entry" + call https "$HLS" /m1/whep/session-1 -X PATCH -H 'Cookie: session=ok' --data-binary 'a=candidate' + expect_code 200 + expect_json .backend mediamtx-whip +} + +test_forward_auth_receives_only_the_cookie() { + auth_log '?clear' >/dev/null + call https "$HLS" /m1/index.m3u8 -H 'Cookie: session=ok' -H 'Authorization: Bearer x' -H 'X-Probe: 1' \ + -H 'X-Forwarded-Uri: /other/index.m3u8' + expect_code 200 + local headers + headers="$(auth_log | jq -c '.[-1].headers')" + [ "$(jq -r .cookie <<<"$headers")" = session=ok ] || fail "$headers" + [ "$(jq -r '.authorization // "none"' <<<"$headers")" = none ] || fail "Authorization reached the api: $headers" + [ "$(jq -r '.["x-probe"] // "none"' <<<"$headers")" = none ] || fail "X-Probe reached the api: $headers" + [ "$(jq -r '.["x-forwarded-uri"]' <<<"$headers")" = /m1/index.m3u8 ] || fail "the client's X-Forwarded-Uri won: $headers" +} + +test_spoofed_preflight_headers_do_not_bypass_forward_auth() { + local spoof + for spoof in 'X-Forwarded-Method: OPTIONS' 'X-Original-Method: OPTIONS'; do + call https "$HLS" /m1/index.m3u8 -H "$spoof" + expect_code 401 + call https "$HLS" /m1/whep -X POST -H "$spoof" --data-binary 'v=0' + expect_code 401 + done +} + +test_mediamtx_paths_outside_its_patterns_are_404() { + local path + for path in /m1 /m1/a/b.ts /m1/whep/a/b; do + call https "$HLS" "$path" -H 'Cookie: session=ok' + expect_code 404 + done +} + +test_snapshot_is_rewritten_with_open_cors() { + call https "$HLS" '/snapshot/abc-123?t=1' -H 'Origin: https://elsewhere.test' + expect_code 200 + expect_json .backend api + expect_json .path '/game-streamer/abc-123/snapshot?t=1' + expect_header Access-Control-Allow-Origin '*' +} + +test_http_redirects_to_https_without_a_port() { + local host + for host in "$WEB" "$API" "$DEMOS" "$WS" "$RELAY" "$SEARCH" "$CONSOLE" "$HLS"; do + call http "$host" '/x/y?z=1' + expect_code 308 + expect_header Location "https://$host/x/y?z=1" + done +} + +test_client_ip_reaches_the_backend() { + call https "$API" /me + expect_json '.headers["x-forwarded-for"]' "$CLIENT_IP" + expect_json '.headers["x-real-ip"]' "$CLIENT_IP" + expect_json '.headers["x-forwarded-proto"]' https +} + +test_other_namespaces_attach_to_the_shared_listener() { + deploy_stub other "80:other" "other:80" + kubectl apply -f - >/dev/null <<'EOF' +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: other + namespace: other +spec: + parentRefs: + - name: traefik-gateway + namespace: traefik + sectionName: web + hostnames: + - other.test + rules: + - backendRefs: + - name: other + port: 80 +EOF + wait_for 200 http other.test /x + expect_json .backend other + call http unknown.test / + expect_code 404 +} + +start_long_requests() { + client sh -c 'head -c 125829120 /dev/zero >/tmp/big' + docker exec -d "$PREFIX-client" sh -c "curl -sk --resolve $DEMOS:443:$NODE_IP -X PUT --data-binary @/tmp/big \ + --limit-rate 1500K -o /tmp/upload.json -w '%{http_code} %{time_total}' \ + 'https://$DEMOS/5stack/demos/big.dem?partNumber=1' >/tmp/upload.out 2>&1" + docker exec -d "$PREFIX-client" sh -c "curl -sk -N --resolve $DEMOS:443:$NODE_IP -w '\n%{http_code} %{time_total}' \ + 'https://$DEMOS/demos/stream?seconds=70' >/tmp/stream.out 2>&1" + docker exec -d "$PREFIX-client" sh -c "python3 /client.py ws $NODE_IP $WS /live 70 >/tmp/ws.json" +} + +await_long_requests() { + for _ in $(seq 1 90); do + if client test -s /tmp/upload.out && client sh -c 'tail -n 1 /tmp/stream.out | grep -qE "^[0-9]{3} [0-9.]+$"' && client test -s /tmp/ws.json; then + return 0 + fi + sleep 2 + done +} + +test_an_upload_over_100mb_running_past_60s_completes() { + local out + out="$(client cat /tmp/upload.out)" + [ "${out%% *}" = 200 ] || fail "upload: $out $(client cat /tmp/upload.json)" + awk -v t="${out##* }" 'BEGIN { exit !(t > 60) }' || fail "the upload took only ${out##* }s" + [ "$(client jq -r .body_bytes /tmp/upload.json)" = 125829120 ] || fail "$(client cat /tmp/upload.json)" +} + +test_a_response_streaming_past_60s_completes() { + local last ticks + last="$(client tail -n 1 /tmp/stream.out)" + ticks="$(client grep -c '^tick ' /tmp/stream.out)" + [ "${last%% *}" = 200 ] && [ "$ticks" = 70 ] || fail "stream ended after $ticks ticks: $last" + awk -v t="${last##* }" 'BEGIN { exit !(t > 60) }' || fail "the stream took only ${last##* }s" +} + +test_a_websocket_held_past_60s_stays_open() { + local out + out="$(client cat /tmp/ws.json)" + [ "$(jq -r .error <<<"$out")" = null ] || fail "$out" + [ "$(jq -r .held <<<"$out")" -ge 65 ] && [ "$(jq -r .echoes <<<"$out")" -ge 7 ] || fail "$out" +} + +test_http_mode_serves_every_host_without_redirect() { + wait_for 200 http "$API" /me + expect_json .backend api + expect_json '.headers["x-forwarded-proto"]' http + wait_for 200 http "$WS" /x + expect_json .path /ws/x + call http "$HLS" /m1/index.m3u8 + expect_code 401 + call http "$HLS" /m1/index.m3u8 -H 'Cookie: session=ok' + expect_code 200 + call http "$HLS" /m1/whep -X OPTIONS -H "Origin: $ORIGIN" -H 'Access-Control-Request-Method: POST' + expect_code 200 + expect_header Access-Control-Allow-Origin "$ORIGIN" + call http "$DEMOS" '/5stack/demos/a.dem?X-Amz-Signature=x' + expect_json .backend rustfs + expect_json .host "$DEMOS" +} + +start_cluster || exit 1 +make_panel || exit 1 +install_traefik "$PANEL_DIR/overlays/config/traefik-config.env" || exit 1 +deploy_panel_stubs || exit 1 +routing_layer https || exit 1 +solver_ingress "$API" "$HLS" | kubectl apply -f - >/dev/null || exit 1 + +run_test test_http01_is_answered_beside_the_https_redirect + +tls_secret 5stack-ssl "$WEB" "$API" "$DEMOS" "$WS" "$RELAY" "$SEARCH" "$CONSOLE" || exit 1 +run_test test_hosts_serve_while_the_stream_certificate_is_missing +tls_secret 5stack-mediamtx-ssl "$HLS" || exit 1 +call https "$HLS" /m1/index.m3u8 +for _ in $(seq 1 30); do + [ "$CODE" = 401 ] && break + sleep 2 + call https "$HLS" /m1/index.m3u8 +done + +start_long_requests +for name in \ + test_api_allowlist_paths_reach_their_backends \ + test_paths_outside_the_allowlist_are_404 \ + test_hasura_healthz_is_rewritten \ + test_web_host_sends_its_api_paths_to_the_api \ + test_demos_reach_rustfs_with_host_and_path_as_signed \ + test_a_64mib_part_put_reaches_rustfs_whole \ + test_ws_and_relay_hosts_rewrite_into_the_api \ + test_websocket_upgrades_through_the_ws_host \ + test_typesense_and_s3_console_hosts \ + test_mediamtx_preflight_is_answered_before_forward_auth \ + test_mediamtx_requires_a_session \ + test_whep_post_and_patch_pass_forward_auth \ + test_forward_auth_receives_only_the_cookie \ + test_spoofed_preflight_headers_do_not_bypass_forward_auth \ + test_mediamtx_paths_outside_its_patterns_are_404 \ + test_snapshot_is_rewritten_with_open_cors \ + test_http_redirects_to_https_without_a_port \ + test_client_ip_reaches_the_backend \ + test_other_namespaces_attach_to_the_shared_listener; do + run_test "$name" +done + +await_long_requests +run_test test_an_upload_over_100mb_running_past_60s_completes +run_test test_a_response_streaming_past_60s_completes +run_test test_a_websocket_held_past_60s_stays_open + +routing_layer http || exit 1 +kubectl delete httproute https-redirect -n 5stack >/dev/null +run_test test_http_mode_serves_every_host_without_redirect + +summary diff --git a/tests/gateway/lib.sh b/tests/gateway/lib.sh new file mode 100644 index 0000000..a517dc3 --- /dev/null +++ b/tests/gateway/lib.sh @@ -0,0 +1,269 @@ +#!/bin/bash + +# k3s in Docker, with the panel's routing layer installed by the same functions +# update.sh calls, in front of stub backends named like the real Services. +# Requests come from a separate client container on the same Docker network, +# so the client IP the backends see is a real peer's. One cluster at a time; +# everything is named after $PREFIX and removed on exit. + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEST_ROOT="${TEST_ROOT:-$(cd "$HERE/../.." && pwd)}" +PREFIX="${PREFIX:-gateway-test}" +K3S_IMAGE=rancher/k3s:v1.36.5-k3s1@sha256:057cfd5a9d51b89f1f0ee139c28d5d5fdd5aa7ddeac2742a0ba90c6dbc0329bb +PYTHON_IMAGE=python:3.13.18-alpine3.24@sha256:bbc0290146ebe0a8b9a8ed07d3f2c7bdeb4d917fc93300a4e47490c8b5a11b48 +DOMAIN=5stack.test +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/gateway-test.XXXXXX")" +PANEL_DIR="$WORK_DIR/panel" +KUBECONFIG="$WORK_DIR/kubeconfig" +DEBUG=false +TESTS_PASSED=0 +TESTS_FAILED=0 +export KUBECONFIG PANEL_DIR DEBUG + +cleanup() { + docker rm -f -v "$PREFIX-k3s" "$PREFIX-client" >/dev/null 2>&1 + docker network rm "$PREFIX-net" >/dev/null 2>&1 + rm -rf "$WORK_DIR" +} + +fail() { + echo " $*" + exit 1 +} + +run_test() { + if ("$1"); then + TESTS_PASSED=$((TESTS_PASSED + 1)) + echo "ok $1" + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + echo "FAIL $1" + fi +} + +summary() { + echo + echo "$TESTS_PASSED passed, $TESTS_FAILED failed" + [ "$TESTS_FAILED" -eq 0 ] +} + +client() { + docker exec "$PREFIX-client" "$@" +} + +start_cluster() { + local port + cleanup + mkdir -p "$WORK_DIR" + docker network create "$PREFIX-net" >/dev/null || return 1 + docker run -d --name "$PREFIX-k3s" --hostname "$PREFIX-k3s" --network "$PREFIX-net" --privileged \ + --tmpfs /run --tmpfs /var/run -p 127.0.0.1::6443 "$K3S_IMAGE" server \ + --disable=traefik --disable=metrics-server --disable=local-storage --tls-san 127.0.0.1 >/dev/null || return 1 + port="$(docker port "$PREFIX-k3s" 6443/tcp | head -n 1 | sed 's/.*://')" + for _ in $(seq 1 90); do + docker exec "$PREFIX-k3s" test -s /etc/rancher/k3s/k3s.yaml 2>/dev/null && break + sleep 2 + done + docker exec "$PREFIX-k3s" cat /etc/rancher/k3s/k3s.yaml | sed "s#https://127.0.0.1:6443#https://127.0.0.1:$port#" >"$KUBECONFIG" + for _ in $(seq 1 90); do + kubectl wait --for=condition=Ready node --all --timeout=5s >/dev/null 2>&1 && break + sleep 2 + done + kubectl wait --for=condition=Ready node --all --timeout=5s >/dev/null || return 1 + NODE_IP="$(docker inspect -f "{{(index .NetworkSettings.Networks \"$PREFIX-net\").IPAddress}}" "$PREFIX-k3s")" + + docker run -d --name "$PREFIX-client" --network "$PREFIX-net" "$PYTHON_IMAGE" sleep infinity >/dev/null || return 1 + client apk add --no-cache -q curl jq openssl >/dev/null || return 1 + docker cp "$HERE/client.py" "$PREFIX-client:/client.py" >/dev/null + # shellcheck disable=SC2034 + CLIENT_IP="$(docker inspect -f "{{(index .NetworkSettings.Networks \"$PREFIX-net\").IPAddress}}" "$PREFIX-client")" +} + +set_env() { + local file="$PANEL_DIR/$1" + [ -f "$file" ] || cp "$file.example" "$file" + grep -v "^$2=" "$file" >"$file.tmp" || true + printf '%s=%s\n' "$2" "$3" >>"$file.tmp" + mv "$file.tmp" "$file" +} + +# A copy of the checkout configured as setup-env.sh would leave it, with the +# panel's own scripts sourced from it. +make_panel() { + local dir file + mkdir -p "$PANEL_DIR" + (cd "$TEST_ROOT" && tar --exclude=./.git --exclude=./kustomize -cf - .) | (cd "$PANEL_DIR" && tar -xf -) + for dir in overlays/config overlays/local-secrets overlays/mediamtx overlays/coturn; do + for file in "$PANEL_DIR/$dir"/*.env.example; do + cp "$file" "${file%.example}" + done + done + set_env overlays/config/api-config.env WEB_DOMAIN "$DOMAIN" + set_env overlays/config/api-config.env API_DOMAIN "api.$DOMAIN" + set_env overlays/config/api-config.env WS_DOMAIN "ws.$DOMAIN" + set_env overlays/config/api-config.env RELAY_DOMAIN "tv.$DOMAIN" + set_env overlays/config/api-config.env DEMOS_DOMAIN "demos.$DOMAIN" + set_env overlays/config/api-config.env GAME_STREAM_DOMAIN "hls.$DOMAIN" + set_env overlays/config/api-config.env MAIL_FROM "hello@$DOMAIN" + set_env overlays/config/s3-config.env S3_CONSOLE_HOST "console.$DOMAIN" + set_env overlays/config/typesense-config.env TYPESENSE_HOST "search.$DOMAIN" + set_env overlays/mediamtx/mediamtx.env GAME_STREAM_DOMAIN "hls.$DOMAIN" + + local script + for script in colors.sh output_redirect.sh apply_overlay.sh setup_kustomize.sh write_overlays.sh \ + install_traefik.sh retire_ingress_nginx.sh; do + # shellcheck source=/dev/null + source "$PANEL_DIR/utils/$script" + done + setup_kustomize >/dev/null || return 1 +} + +# The kinds of the overlay update.sh applies that make up the routing layer. +# The rest (databases, the api itself) is what the stub backends stand in for. +routing_layer() { + local protocol="$1" + write_overlays + "$PANEL_DIR/kustomize" build "$PANEL_DIR/overlays/local-secrets-$protocol" >"$WORK_DIR/overlay.yaml" || return 1 + python3 -I - "$WORK_DIR/overlay.yaml" "$WORK_DIR/routing.yaml" "$WORK_DIR/issuer.yaml" <<'PY' +import re, sys +docs = open(sys.argv[1]).read().split("\n---\n") +kind = lambda d: (re.search(r"(?m)^kind: (\S+)$", d) or [None, None])[1] +routing = [d for d in docs if kind(d) in ("Namespace", "Gateway", "HTTPRoute", "Middleware", "IngressClass")] +open(sys.argv[2], "w").write("\n---\n".join(routing) + "\n") +open(sys.argv[3], "w").write("\n---\n".join(d for d in docs if kind(d) == "Issuer") + "\n") +PY + kubectl apply -f "$WORK_DIR/routing.yaml" >/dev/null +} + +# Pods running backend.py, listening on PORTS, behind one Service per entry +# of SERVICES ("name:port[,port]"). +deploy_stub() { + local ns="$1" ports="$2" service svc_name svc_ports port + shift 2 + kubectl create namespace "$ns" --dry-run=client -o yaml | kubectl apply -f - >/dev/null + kubectl -n "$ns" create configmap stub-backend --from-file=backend.py="$HERE/backend.py" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null + kubectl apply -f - >/dev/null </dev/null </dev/null +} + +deploy_panel_stubs() { + deploy_stub 5stack \ + "5585:api,8080:hasura,3000:web,9000:rustfs,9001:rustfs-console,8108:typesense,8888:mediamtx-hls,8889:mediamtx-whip,8089:acme-solver" \ + "api:5585" "hasura:8080" "web:3000" "rustfs:9000 9001" "typesense:8108" "mediamtx:8888 8889" "acme-solver:8089" +} + +# A self-signed kubernetes.io/tls Secret in 5stack for the given hosts. +tls_secret() { + local name="$1" san="" host + shift + for host in "$@"; do + san="${san:+$san,}DNS:$host" + done + client openssl req -x509 -newkey rsa:2048 -nodes -days 2 -subj "/CN=$1" -addext "subjectAltName=$san" \ + -keyout /tmp/tls.key -out /tmp/tls.crt >/dev/null 2>&1 || return 1 + docker cp "$PREFIX-client:/tmp/tls.key" "$WORK_DIR/tls.key" >/dev/null + docker cp "$PREFIX-client:/tmp/tls.crt" "$WORK_DIR/tls.crt" >/dev/null + kubectl -n 5stack create secret tls "$name" --cert="$WORK_DIR/tls.crt" --key="$WORK_DIR/tls.key" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null +} + +# One request from the client container: sets CODE, HEADERS and BODY. +# call https api.5stack.test /me [curl args...] +call() { + local scheme="$1" host="$2" path="$3" port=80 out rest + shift 3 + [ "$scheme" = https ] && port=443 + LAST="$scheme://$host$path" + out="$(client sh -c 'rm -f /tmp/body /tmp/headers; curl -sk -o /tmp/body -D /tmp/headers -w "%{http_code}" --max-time 30 "$@"; echo; cat /tmp/headers; echo "@@BODY@@"; cat /tmp/body 2>/dev/null' \ + sh --resolve "$host:$port:$NODE_IP" "$@" "$scheme://$host$path")" + CODE="${out%%$'\n'*}" + rest="${out#*$'\n'}" + HEADERS="${rest%%@@BODY@@*}" + BODY="${rest#*@@BODY@@}" + BODY="${BODY#$'\n'}" +} + +header() { + printf '%s\n' "$HEADERS" | grep -i "^$1:" | head -n 1 | cut -d: -f2- | tr -d '\r' | sed 's/^ //' +} + +json() { + jq -r "$1" <<<"$BODY" 2>/dev/null +} + +expect_code() { + [ "$CODE" = "$1" ] || fail "$LAST: expected HTTP $1, got $CODE ${BODY:0:200}" +} + +expect_json() { + local got + got="$(json "$1")" + [ "$got" = "$2" ] || fail "$LAST: expected $1 = '$2', got '$got'" +} + +expect_header() { + local got + got="$(header "$1")" + [ "$got" = "$2" ] || fail "$LAST: expected header $1: '$2', got '$got'" +} + +# Polls until a request answers with the given code: routes take a moment to +# reach Traefik after an apply. +wait_for() { + local want="$1" + shift + for _ in $(seq 1 60); do + call "$@" + [ "$CODE" = "$want" ] && return 0 + sleep 2 + done + fail "$LAST never answered $want (last $CODE)" +} + +auth_log() { + call https "demos.$DOMAIN" "/demos/__auth_log$1" + printf '%s' "$BODY" +} diff --git a/tests/gateway/upgrade_test.sh b/tests/gateway/upgrade_test.sh new file mode 100644 index 0000000..0c91d1f --- /dev/null +++ b/tests/gateway/upgrade_test.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# An install that is still on ingress-nginx -- the controller, its admission +# webhook, the panel's Ingresses as they were, and another app's nginx-class +# Ingress with forward-auth -- taken through update.sh's routing steps: +# install_traefik, the overlay's Gateway and routes, retire_ingress_nginx. + +# shellcheck source=tests/gateway/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +trap cleanup EXIT + +INGRESS_NGINX_MANIFEST=https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.9/deploy/static/provider/baremetal/deploy.yaml +INGRESS_NGINX_SHA256=946884f0b417d7af1bc50d8896bd8c201b90fda25813cf797e050ee19b53e1c4 +API="api.$DOMAIN" +HLS="hls.$DOMAIN" +ORIGIN="https://$DOMAIN" + +# Applies while ingress-nginx's admission webhook may still be starting. +apply_with_retries() { + for _ in $(seq 1 30); do + kubectl apply -f "$1" >/dev/null 2>&1 && return 0 + sleep 4 + done + kubectl apply -f "$1" +} + +install_ingress_nginx() { + curl -fsSL "$INGRESS_NGINX_MANIFEST" -o "$WORK_DIR/ingress-nginx.yaml" || return 1 + echo "$INGRESS_NGINX_SHA256 $WORK_DIR/ingress-nginx.yaml" | shasum -a 256 -c - >/dev/null || return 1 + kubectl apply -f "$WORK_DIR/ingress-nginx.yaml" >/dev/null || return 1 + kubectl -n ingress-nginx rollout status deployment/ingress-nginx-controller --timeout=300s >/dev/null || return 1 + apply_with_retries "$HERE/fixtures/ingress-nginx-panel.yaml" +} + +# Another app on the same cluster that stays on an nginx-class Ingress: plain +# HTTP, its own forward-auth with a sign-in redirect, a body limit, no +# buffering and long timeouts. +deploy_other_app() { + deploy_stub qx "80:qx,8000:qx-auth" "qx-app:80" "qx-auth:8000" || return 1 + cat >"$WORK_DIR/qx-ingress.yaml" <<'EOF' +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: qx-control + namespace: qx + annotations: + nginx.ingress.kubernetes.io/auth-url: http://qx-auth.qx.svc.cluster.local:8000/qx/authorize + nginx.ingress.kubernetes.io/auth-signin: https://qx.test/login + nginx.ingress.kubernetes.io/proxy-body-size: 8m + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" +spec: + ingressClassName: nginx + rules: + - host: qx.test + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: qx-app + port: + number: 80 +EOF + apply_with_retries "$WORK_DIR/qx-ingress.yaml" +} + +expect_other_app_routes_with_its_auth() { + call http qx.test /dashboard + expect_code 302 + case "$(header Location)" in + https://qx.test/login\?rd=*) ;; + *) fail "no sign-in redirect: $(header Location)" ;; + esac + call http qx.test /dashboard -H 'Cookie: session=ok' + expect_code 200 + expect_json .backend qx + expect_json .path /dashboard + client sh -c 'head -c 4194304 /dev/zero >/tmp/small; head -c 10485760 /dev/zero >/tmp/large' + call http qx.test /upload -H 'Cookie: session=ok' --data-binary @/tmp/small + expect_code 200 + expect_json .body_bytes 4194304 + call http qx.test /upload -H 'Cookie: session=ok' --data-binary @/tmp/large + expect_code 413 +} + +test_before_the_update_ingress_nginx_serves_everything() { + wait_for 200 https "$API" /me + expect_json .backend api + [ "$(header Server)" = nginx ] || fail "not ingress-nginx: $(header Server)" + expect_other_app_routes_with_its_auth +} + +test_ingress_nginx_is_gone() { + local phase + phase="$(kubectl get namespace ingress-nginx -o jsonpath='{.status.phase}' 2>/dev/null)" + [ -z "$phase" ] || [ "$phase" = Terminating ] || fail "ingress-nginx namespace is $phase" + kubectl get validatingwebhookconfiguration ingress-nginx-admission >/dev/null 2>&1 && fail "the admission webhook is still there" + kubectl get clusterrole ingress-nginx >/dev/null 2>&1 && fail "the ingress-nginx ClusterRole is still there" + [ "$(kubectl get ingressclass nginx -o jsonpath='{.spec.controller}')" = k8s.io/ingress-nginx ] || fail "IngressClass nginx is gone" + [ -z "$(kubectl -n 5stack get ingress -o name)" ] || fail "panel Ingresses left: $(kubectl -n 5stack get ingress -o name)" +} + +test_panel_routes_go_through_traefik() { + call https "$API" /me + expect_code 200 + expect_json .backend api + [ "$(header Server)" != nginx ] || fail "still answered by ingress-nginx" + call https "$API" /nope + expect_code 404 + call https "ws.$DOMAIN" /x + expect_json .path /ws/x + call https "$HLS" /m1/whep -X OPTIONS -H "Origin: $ORIGIN" -H 'Access-Control-Request-Method: POST' + expect_code 200 + expect_header Access-Control-Allow-Origin "$ORIGIN" + call https "$HLS" /m1/index.m3u8 + expect_code 401 + call https "$HLS" /m1/whep -X POST -H 'Cookie: session=ok' --data-binary 'v=0' + expect_code 201 + call http "$API" /me + expect_code 308 + call https "$API" /me + expect_json '.headers["x-forwarded-for"]' "$CLIENT_IP" +} + +test_the_other_app_keeps_its_nginx_ingress_and_auth() { + [ "$(kubectl -n qx get ingress qx-control -o jsonpath='{.spec.ingressClassName}')" = nginx ] || fail "qx Ingress changed" + expect_other_app_routes_with_its_auth + [ "$(header Server)" != nginx ] || fail "still answered by ingress-nginx" +} + +test_the_switch_costs_seconds() { + local longest + longest="$(jq -r .longest_outage <<<"$POLL")" + echo " outage while ports moved: ${longest}s ($(jq -r '"\(.failed) of \(.requests)"' <<<"$POLL") requests failed)" + awk -v t="$longest" 'BEGIN { exit !(t < 30) }' || fail "$POLL" +} + +test_a_plugin_ingress_pins_a_role_with_auth_snippet() { + kubectl apply -f - >/dev/null </dev/null || fail "install_traefik failed" + routing_layer https || fail "the overlay did not apply" + retire_ingress_nginx || fail "retire_ingress_nginx failed" + call https "$API" /me + expect_code 200 + expect_other_app_routes_with_its_auth +} + +start_cluster || exit 1 +make_panel || exit 1 +install_ingress_nginx || exit 1 +deploy_panel_stubs || exit 1 +tls_secret 5stack-ssl "$DOMAIN" "$API" "demos.$DOMAIN" "ws.$DOMAIN" "tv.$DOMAIN" "search.$DOMAIN" "console.$DOMAIN" || exit 1 +tls_secret 5stack-mediamtx-ssl "$HLS" || exit 1 +deploy_other_app || exit 1 + +run_test test_before_the_update_ingress_nginx_serves_everything + +client rm -f /tmp/stop +docker exec -d "$PREFIX-client" sh -c "python3 /client.py poll $NODE_IP $API /me /tmp/stop >/tmp/poll.json" +sleep 2 +install_traefik "$PANEL_DIR/overlays/config/traefik-config.env" || exit 1 +routing_layer https || exit 1 +retire_ingress_nginx || exit 1 +sleep 5 +client touch /tmp/stop +for _ in $(seq 1 10); do + client test -s /tmp/poll.json && break + sleep 1 +done +POLL="$(client cat /tmp/poll.json)" + +for name in \ + test_ingress_nginx_is_gone \ + test_panel_routes_go_through_traefik \ + test_the_other_app_keeps_its_nginx_ingress_and_auth \ + test_the_switch_costs_seconds \ + test_a_plugin_ingress_pins_a_role_with_auth_snippet \ + test_running_the_update_again_changes_nothing; do + run_test "$name" +done + +summary diff --git a/tests/scripts/setup_env_test.sh b/tests/scripts/setup_env_test.sh index 7bb2924..f96f700 100644 --- a/tests/scripts/setup_env_test.sh +++ b/tests/scripts/setup_env_test.sh @@ -151,4 +151,14 @@ test_a_config_without_a_final_newline_keeps_its_last_line() { assert_eq "$(env_lines overlays/config/api-config.env AUTH_COOKIE_DOMAIN)" "AUTH_COOKIE_DOMAIN=.example.com" } +test_custom_ports_move_from_nginx_config_env_to_traefik_config_env() { + make_panel + set_env overlays/config/api-config.env WEB_DOMAIN panel.example.com + printf 'HTTP_PORT=8080\nHTTPS_PORT=8443\n' >"$PANEL/overlays/config/nginx-config.env" + run_setup_env + assert_eq "$(env_lines overlays/config/traefik-config.env HTTP_PORT)" "HTTP_PORT=8080" + assert_eq "$(env_lines overlays/config/traefik-config.env HTTPS_PORT)" "HTTPS_PORT=8443" + [ ! -e "$PANEL/overlays/config/nginx-config.env" ] || fail "nginx-config.env was left behind" +} + run_tests diff --git a/update.sh b/update.sh index bc78ff9..6f664a6 100755 --- a/update.sh +++ b/update.sh @@ -23,6 +23,8 @@ if [ "$REVERSE_PROXY" = true ]; then for CERT in 5stack-ssl 5stack-mediamtx-ssl; do kubectl --kubeconfig=$KUBECONFIG delete certificate "$CERT" -n 5stack 2>/dev/null done + # Behind a proxy that already terminated TLS, this would loop forever. + kubectl --kubeconfig=$KUBECONFIG delete httproute https-redirect -n 5stack 2>/dev/null fi if [ "$REVERSE_PROXY" != true ]; then diff --git a/utils/install_traefik.sh b/utils/install_traefik.sh index 3cce98b..41e9557 100644 --- a/utils/install_traefik.sh +++ b/utils/install_traefik.sh @@ -79,7 +79,8 @@ chart_archive() { if tar --version 2>/dev/null | grep -q GNU; then tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner --exclude='.*' -cf - -C "$parent" "$name" | gzip -n else - tar --exclude='.*' -cf - -C "$parent" "$name" | gzip -n + # macOS tar otherwise adds ._ AppleDouble entries, which Helm rejects. + COPYFILE_DISABLE=1 tar --exclude='.*' -cf - -C "$parent" "$name" | gzip -n fi } @@ -136,8 +137,8 @@ install_helm_chart() { echo "Waiting for helm-controller to apply $name..." deadline=$((SECONDS + 600)) while true; do - IFS=$'\t' read -r uid complete failed < <(kubectl "${kc[@]}" get job "helm-install-$name" -n kube-system \ - -o jsonpath='{.metadata.uid}{"\t"}{.status.conditions[?(@.type=="Complete")].status}{"\t"}{.status.conditions[?(@.type=="Failed")].status}{"\n"}' 2>/dev/null) + IFS='|' read -r uid complete failed < <(kubectl "${kc[@]}" get job "helm-install-$name" -n kube-system \ + -o jsonpath='{.metadata.uid}{"|"}{.status.conditions[?(@.type=="Complete")].status}{"|"}{.status.conditions[?(@.type=="Failed")].status}{"\n"}' 2>/dev/null) if [ -n "$uid" ] && [ "$uid" != "$old_uid" ]; then if [ "$complete" = "True" ]; then return 0 diff --git a/utils/retire_ingress_nginx.sh b/utils/retire_ingress_nginx.sh index fc15404..a0f404e 100644 --- a/utils/retire_ingress_nginx.sh +++ b/utils/retire_ingress_nginx.sh @@ -23,6 +23,13 @@ retire_ingress_nginx() { output_redirect kubectl "${kc[@]}" delete validatingwebhookconfiguration ingress-nginx-admission --ignore-not-found if kubectl "${kc[@]}" get service ingress-nginx-controller -n ingress-nginx >/dev/null 2>&1; then + # Not before Traefik has taken up the panel's routes, or it would be + # handed the ports with nothing to serve on them. + if ! output_redirect kubectl "${kc[@]}" wait httproute/api -n 5stack --timeout=120s \ + --for=jsonpath='{.status.parents[0].conditions[?(@.type=="Accepted")].status}'=True; then + err "Traefik has not accepted the panel's routes; ingress-nginx is left serving" + return 1 + fi echo "Moving ports 80/443 from ingress-nginx to Traefik..." output_redirect kubectl "${kc[@]}" delete service ingress-nginx-controller -n ingress-nginx --wait=true fi From 7e50aee4a0ac3b3a83b2710dea26e44cf267056f Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 28 Oct 2026 13:32:05 -0400 Subject: [PATCH 03/17] test: the upgrade test creates the 5stack namespace before the old Ingresses, and http mode waits out the removed redirect --- tests/gateway/upgrade_test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/gateway/upgrade_test.sh b/tests/gateway/upgrade_test.sh index 0c91d1f..626e8ef 100644 --- a/tests/gateway/upgrade_test.sh +++ b/tests/gateway/upgrade_test.sh @@ -29,6 +29,7 @@ install_ingress_nginx() { echo "$INGRESS_NGINX_SHA256 $WORK_DIR/ingress-nginx.yaml" | shasum -a 256 -c - >/dev/null || return 1 kubectl apply -f "$WORK_DIR/ingress-nginx.yaml" >/dev/null || return 1 kubectl -n ingress-nginx rollout status deployment/ingress-nginx-controller --timeout=300s >/dev/null || return 1 + kubectl create namespace 5stack >/dev/null || return 1 apply_with_retries "$HERE/fixtures/ingress-nginx-panel.yaml" } From 4f55ddd261b929a02e02b0ee1d3026df0344ed7b Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 28 Oct 2026 20:46:47 -0400 Subject: [PATCH 04/17] fix: update.sh keeps the Traefik install and ingress-nginx retirement through the merge with main --- update.sh | 61 +++++++++++-------------------------------------------- 1 file changed, 12 insertions(+), 49 deletions(-) diff --git a/update.sh b/update.sh index 1168181..6f664a6 100755 --- a/update.sh +++ b/update.sh @@ -23,6 +23,8 @@ if [ "$REVERSE_PROXY" = true ]; then for CERT in 5stack-ssl 5stack-mediamtx-ssl; do kubectl --kubeconfig=$KUBECONFIG delete certificate "$CERT" -n 5stack 2>/dev/null done + # Behind a proxy that already terminated TLS, this would loop forever. + kubectl --kubeconfig=$KUBECONFIG delete httproute https-redirect -n 5stack 2>/dev/null fi if [ "$REVERSE_PROXY" != true ]; then @@ -45,10 +47,14 @@ if [ "$REVERSE_PROXY" != true ]; then ok "cert-manager is ready" fi -step "Building overlay manifests" -HTTP_REPLACEMENTS="$PANEL_DIR/overlays/http/http-replacements.yaml" -HTTPS_REPLACEMENTS="$PANEL_DIR/overlays/http/https-replacements.yaml" +# Before the overlay: its Gateway, routes and Middlewares need the CRDs, and +# on an upgrade from ingress-nginx Traefik has to be serving before it is +# handed ports 80/443. +step "Installing Traefik" +install_traefik || die "failed to install Traefik" +ok "Traefik is ready" +step "Building overlay manifests" # Whether an operator has asked for a TURN relay at all. Read straight from # coturn's own env file rather than the environment: this is the same file the # generated ConfigMap is built from, so the two can never disagree. @@ -56,48 +62,7 @@ TURN_DOMAIN="$(grep -s '^TURN_DOMAIN=' "$PANEL_DIR/overlays/coturn/coturn.env" | TURN_DOMAIN="${TURN_DOMAIN%\"}" TURN_DOMAIN="${TURN_DOMAIN#\"}" -OVERLAY_BASES=("vault" "local-secrets") -for BASE in "${OVERLAY_BASES[@]}"; do - for PROTOCOL in "http" "https"; do - OVERLAY="overlays/${BASE}-${PROTOCOL}" - mkdir -p "$OVERLAY" - # MediaMTX is a WebRTC relay, not an encoder, and it now also carries - # player cameras — which have nothing to do with GPU game streaming. - # Only the nvidia device plugin stays GPU-gated. - STREAMING_RESOURCES="$(if [[ "$PROTOCOL" == "https" ]]; then echo "- ../mediamtx-https"; else echo "- ../mediamtx"; fi)" - # The TURN relay is opt-in and most installs never need one: WebRTC - # connects directly for the large majority of players, and STUN covers - # most of the rest. Deployed only once TURN_DOMAIN is set, so nobody - # ends up running a relay -- on hostNetwork, holding port 3478 -- that - # they never asked for. It has no http/https split: it speaks TURN, not - # HTTP, and the media it relays is already DTLS-encrypted end to end. - if [ -n "$TURN_DOMAIN" ]; then - STREAMING_RESOURCES="$STREAMING_RESOURCES -- ../coturn" - fi - if [ "$GPU_VENDOR" = "nvidia" ]; then - STREAMING_RESOURCES="- ../nvidia -$STREAMING_RESOURCES" - fi - - cat > "$OVERLAY/kustomization.yaml" <> "$OVERLAY/kustomization.yaml" - else - cat "$HTTP_REPLACEMENTS" >> "$OVERLAY/kustomization.yaml" - fi - done -done +write_overlays ok "overlays generated" step "Applying kustomize overlay" @@ -139,10 +104,6 @@ fi apply_overlay "$OVERLAY" || die "failed to apply $OVERLAY" ok "overlay applied" -# install.sh does this when it installs ingress-nginx; installs from before -# that only get it here. See utils/install_ingress_nginx.sh. -allow_ingress_nginx_signals || warn "could not let the ingress-nginx controller reload nginx; on Ubuntu 26.04 it will not become Ready" - # Strictly after the apply: it is the apply that removes the cert-manager.io/issuer # annotation from the ingresses, and until that lands ingress-shim recreates an # Ingress-owned Certificate as fast as we can let go of one. @@ -150,6 +111,8 @@ if [ "$REVERSE_PROXY" != true ]; then disown_shim_owned_certificates fi +retire_ingress_nginx || die "Traefik did not take over ports 80/443" + if [ "$VAULT_MANAGER" = true ]; then step "Syncing Vault secrets" if ! resync_vault_secrets; then From 28c8a96eeba8ceae1ecc2b75f13d98cc78ba038f Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 28 Oct 2026 20:53:14 -0400 Subject: [PATCH 05/17] fix: Traefik enforces nginx-class body limits by buffering requests as ingress-nginx did, and the gateway tests pass shellcheck and detect nginx by its install --- charts/gateway/values.yaml | 3 +++ tests/gateway/fresh_install_test.sh | 12 +++++++++--- tests/gateway/upgrade_test.sh | 3 ++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/charts/gateway/values.yaml b/charts/gateway/values.yaml index d83c9d6..b721d8f 100644 --- a/charts/gateway/values.yaml +++ b/charts/gateway/values.yaml @@ -51,6 +51,9 @@ traefik: publishService: enabled: true allowSnippetAnnotations: true + # proxy-body-size is only enforced while request buffering is on, which + # ingress-nginx defaulted to and Traefik does not. + proxyRequestBuffering: true # Traefik v3 cuts any request still being read after 60s, which kills long # uploads, streams and websockets. diff --git a/tests/gateway/fresh_install_test.sh b/tests/gateway/fresh_install_test.sh index 031918b..8662209 100644 --- a/tests/gateway/fresh_install_test.sh +++ b/tests/gateway/fresh_install_test.sh @@ -174,7 +174,9 @@ test_mediamtx_preflight_is_answered_before_forward_auth() { expect_header Access-Control-Allow-Origin "$ORIGIN" expect_header Access-Control-Allow-Credentials true [ -z "$BODY" ] || fail "the preflight reached a backend: $BODY" - [ "$(auth_log | jq length)" = 0 ] || fail "the preflight went through forward-auth" + local seen + seen="$(auth_log)" + [ "$(jq length <<<"$seen")" = 0 ] || fail "the preflight went through forward-auth: $seen" } test_mediamtx_requires_a_session() { @@ -319,7 +321,9 @@ test_a_response_streaming_past_60s_completes() { local last ticks last="$(client tail -n 1 /tmp/stream.out)" ticks="$(client grep -c '^tick ' /tmp/stream.out)" - [ "${last%% *}" = 200 ] && [ "$ticks" = 70 ] || fail "stream ended after $ticks ticks: $last" + if [ "${last%% *}" != 200 ] || [ "$ticks" != 70 ]; then + fail "stream ended after $ticks ticks: $last" + fi awk -v t="${last##* }" 'BEGIN { exit !(t > 60) }' || fail "the stream took only ${last##* }s" } @@ -327,7 +331,9 @@ test_a_websocket_held_past_60s_stays_open() { local out out="$(client cat /tmp/ws.json)" [ "$(jq -r .error <<<"$out")" = null ] || fail "$out" - [ "$(jq -r .held <<<"$out")" -ge 65 ] && [ "$(jq -r .echoes <<<"$out")" -ge 7 ] || fail "$out" + if [ "$(jq -r .held <<<"$out")" -lt 65 ] || [ "$(jq -r .echoes <<<"$out")" -lt 7 ]; then + fail "$out" + fi } test_http_mode_serves_every_host_without_redirect() { diff --git a/tests/gateway/upgrade_test.sh b/tests/gateway/upgrade_test.sh index 626e8ef..77fb3f4 100644 --- a/tests/gateway/upgrade_test.sh +++ b/tests/gateway/upgrade_test.sh @@ -90,7 +90,8 @@ expect_other_app_routes_with_its_auth() { test_before_the_update_ingress_nginx_serves_everything() { wait_for 200 https "$API" /me expect_json .backend api - [ "$(header Server)" = nginx ] || fail "not ingress-nginx: $(header Server)" + kubectl -n ingress-nginx get service ingress-nginx-controller >/dev/null 2>&1 || fail "ingress-nginx is not serving" + kubectl get namespace traefik >/dev/null 2>&1 && fail "Traefik is already installed" expect_other_app_routes_with_its_auth } From dddf6ff5819ca611cb9ccc74d54c26e3e0dde51f Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 28 Oct 2026 20:56:28 -0400 Subject: [PATCH 06/17] test: the gateway tests wait for the other app's nginx route, and count only the preflight's own path at forward-auth --- tests/gateway/fresh_install_test.sh | 2 +- tests/gateway/upgrade_test.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/gateway/fresh_install_test.sh b/tests/gateway/fresh_install_test.sh index 8662209..e485936 100644 --- a/tests/gateway/fresh_install_test.sh +++ b/tests/gateway/fresh_install_test.sh @@ -175,7 +175,7 @@ test_mediamtx_preflight_is_answered_before_forward_auth() { expect_header Access-Control-Allow-Credentials true [ -z "$BODY" ] || fail "the preflight reached a backend: $BODY" local seen - seen="$(auth_log)" + seen="$(auth_log | jq '[.[] | select(.headers["x-forwarded-uri"] | startswith("/m1/whep"))]')" [ "$(jq length <<<"$seen")" = 0 ] || fail "the preflight went through forward-auth: $seen" } diff --git a/tests/gateway/upgrade_test.sh b/tests/gateway/upgrade_test.sh index 77fb3f4..793dbfb 100644 --- a/tests/gateway/upgrade_test.sh +++ b/tests/gateway/upgrade_test.sh @@ -92,6 +92,7 @@ test_before_the_update_ingress_nginx_serves_everything() { expect_json .backend api kubectl -n ingress-nginx get service ingress-nginx-controller >/dev/null 2>&1 || fail "ingress-nginx is not serving" kubectl get namespace traefik >/dev/null 2>&1 && fail "Traefik is already installed" + wait_for 302 http qx.test /dashboard expect_other_app_routes_with_its_auth } From 8cd5293f909b69ac67ea75a1a5978d88aa8b2e24 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 28 Oct 2026 21:12:48 -0400 Subject: [PATCH 07/17] fix: an update keeps the ports ingress-nginx served on, a re-run recovers a failed Traefik release, a stalled handover gives nginx its ports back, and the Media Server page's stats route survives --- .github/workflows/gateway-chart.yml | 15 ++- .github/workflows/panel-scripts.yml | 1 + base/api/httproute.yaml | 3 + dev.sh | 2 +- overlays/mediamtx/middlewares.yaml | 2 + tests/gateway/fresh_install_test.sh | 4 +- tests/gateway/lib.sh | 2 +- tests/scripts/stub/kubectl | 32 ++++++ tests/scripts/traefik_install_test.sh | 147 ++++++++++++++++++++++++++ utils/install_traefik.sh | 64 ++++++++++- utils/retire_ingress_nginx.sh | 48 ++++++++- 11 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 tests/scripts/traefik_install_test.sh diff --git a/.github/workflows/gateway-chart.yml b/.github/workflows/gateway-chart.yml index dad2953..b32afe2 100644 --- a/.github/workflows/gateway-chart.yml +++ b/.github/workflows/gateway-chart.yml @@ -25,9 +25,16 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin + # A published version is never pushed again: whoever installed it keeps + # getting the chart they installed. Bump Chart.yaml's version to publish. - run: | - helm package charts/gateway-crds --destination dist - helm package charts/gateway --destination dist - for chart in dist/*.tgz; do - helm push "$chart" oci://ghcr.io/5stackgg/charts + for dir in charts/gateway-crds charts/gateway; do + name="$(yq -r .name "$dir/Chart.yaml")" + version="$(yq -r .version "$dir/Chart.yaml")" + if helm show chart "oci://ghcr.io/5stackgg/charts/$name" --version "$version" >/dev/null 2>&1; then + echo "$name $version is already published" + continue + fi + helm package "$dir" --destination dist + helm push "dist/$name-$version.tgz" oci://ghcr.io/5stackgg/charts done diff --git a/.github/workflows/panel-scripts.yml b/.github/workflows/panel-scripts.yml index db319af..eaf0ba5 100644 --- a/.github/workflows/panel-scripts.yml +++ b/.github/workflows/panel-scripts.yml @@ -45,4 +45,5 @@ jobs: - run: bash tests/scripts/system_upgrade_test.sh - run: bash tests/scripts/plugin_test.sh - run: bash tests/scripts/node_ip_test.sh + - run: bash tests/scripts/traefik_install_test.sh - run: bash tests/scripts/dev_game_server_test.sh diff --git a/base/api/httproute.yaml b/base/api/httproute.yaml index f3b8430..c6200d8 100644 --- a/base/api/httproute.yaml +++ b/base/api/httproute.yaml @@ -38,6 +38,9 @@ spec: - path: type: Exact value: /system/logs/download + - path: + type: Exact + value: /mediamtx/stats - path: type: PathPrefix value: /branding diff --git a/dev.sh b/dev.sh index 7112d8d..d31fce0 100755 --- a/dev.sh +++ b/dev.sh @@ -57,7 +57,7 @@ export KUBECONFIG=~/.kube/5stack-dev kubectl config use-context k3d-5stack-dev install_traefik "$PANEL_DIR/overlays/dev/config/traefik-config.env" || exit 1 -retire_ingress_nginx || exit 1 +retire_ingress_nginx --immediately || exit 1 echo "Labeling node..." kubectl label node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') 5stack-api=true 5stack-hasura=true 5stack-minio=true 5stack-rustfs=true 5stack-timescaledb=true 5stack-redis=true 5stack-typesense=true 5stack-web=true 5stack-dev-server=true 5stack-mediamtx=true diff --git a/overlays/mediamtx/middlewares.yaml b/overlays/mediamtx/middlewares.yaml index 8499012..f08831b 100644 --- a/overlays/mediamtx/middlewares.yaml +++ b/overlays/mediamtx/middlewares.yaml @@ -19,6 +19,7 @@ spec: accessControlAllowHeaders: [Content-Type, Accept, Authorization, If-Match] # The WHEP answer's Location is the session URL trickle ICE PATCHes to. accessControlExposeHeaders: [Location] + accessControlMaxAge: 600 addVaryHeader: true --- # Require login to watch: every stream request is authorized by the API, @@ -54,6 +55,7 @@ spec: accessControlAllowOriginList: ["*"] accessControlAllowMethods: [GET, OPTIONS] accessControlAllowHeaders: [Content-Type, Accept, If-None-Match] + accessControlMaxAge: 600 --- apiVersion: traefik.io/v1alpha1 kind: Middleware diff --git a/tests/gateway/fresh_install_test.sh b/tests/gateway/fresh_install_test.sh index e485936..076c035 100644 --- a/tests/gateway/fresh_install_test.sh +++ b/tests/gateway/fresh_install_test.sh @@ -78,7 +78,7 @@ test_api_allowlist_paths_reach_their_backends() { /game-server-node/script:api /game-server-node/ping/1:api /avatars/1.png:api /trophies/x:api \ /branding/logo:api /news/1:api /utility/x:api /custom-pages/x:api /plugins/authorize/x:api \ /huds/x.zip:api /events/media/1:api /snapshots/1:api /steam-match-history/upload:api /clip-views/1:api \ - /voice/1:api /notifications/1:api /chat/1:api /sanctions/server/1:api /file-manager/upload:api \ + /voice/1:api /notifications/1:api /chat/1:api /sanctions/server/1:api /file-manager/upload:api /mediamtx/stats:api \ /v1/graphql:hasura /v1alpha1/x:hasura /v2/query:hasura; do path="${entry%:*}" backend="${entry##*:}" @@ -91,7 +91,7 @@ test_api_allowlist_paths_reach_their_backends() { test_paths_outside_the_allowlist_are_404() { local path - for path in / /nope /matchesx /me/x /system/healthz/x /system/logs /v3 /ws /auth/steam; do + for path in / /nope /matchesx /me/x /system/healthz/x /system/logs /v3 /ws /auth/steam /mediamtx; do call https "$API" "$path" expect_code 404 done diff --git a/tests/gateway/lib.sh b/tests/gateway/lib.sh index a517dc3..470131f 100644 --- a/tests/gateway/lib.sh +++ b/tests/gateway/lib.sh @@ -111,7 +111,7 @@ make_panel() { local script for script in colors.sh output_redirect.sh apply_overlay.sh setup_kustomize.sh write_overlays.sh \ - install_traefik.sh retire_ingress_nginx.sh; do + update_env_var.sh install_traefik.sh retire_ingress_nginx.sh; do # shellcheck source=/dev/null source "$PANEL_DIR/utils/$script" done diff --git a/tests/scripts/stub/kubectl b/tests/scripts/stub/kubectl index 8700fc1..064d6ef 100755 --- a/tests/scripts/stub/kubectl +++ b/tests/scripts/stub/kubectl @@ -19,6 +19,38 @@ while [ $# -gt 0 ]; do done case "$args" in + *" get crd helmcharts.helm.cattle.io "* | *" apply --server-side "*) ;; + *" apply -f - "*) cat >"$WORK_DIR/applied" ;; + *" get service traefik "*) if [ -f "$WORK_DIR/traefik-address" ]; then cat "$WORK_DIR/traefik-address"; fi ;; + *" get helmchart "*) echo 1 ;; + *" get job helm-install-"*) + case "$args" in + *'uid}{"|"}'*) + if [ -f "$WORK_DIR/job-retried" ]; then + echo "uid-2|True|" + elif [ -f "$WORK_DIR/job-failed" ]; then + echo "uid-1||True" + else + echo "uid-1|True|" + fi + ;; + *'type=="Failed"'*) if [ -f "$WORK_DIR/job-failed" ]; then echo True; fi ;; + *) echo uid-1 ;; + esac + ;; + *" logs job/helm-install-"*) if [ -f "$WORK_DIR/job-logs" ]; then cat "$WORK_DIR/job-logs"; fi ;; + *" patch helmchart "* | *" delete job helm-install-"*) touch "$WORK_DIR/job-retried" ;; + *" get service ingress-nginx-controller "*) + case "$args" in + *'name=="https"'*) port_file="$WORK_DIR/nginx-https-port" ;; + *) port_file="$WORK_DIR/nginx-http-port" ;; + esac + if [ ! -f "$port_file" ]; then + echo 'Error from server (NotFound): services "ingress-nginx-controller" not found' >&2 + exit 1 + fi + cat "$port_file" + ;; *" get nodes "*) if [ -f "$WORK_DIR/nodes" ]; then cat "$WORK_DIR/nodes"; fi if [ -f "$WORK_DIR/nodes.next" ]; then mv "$WORK_DIR/nodes.next" "$WORK_DIR/nodes"; fi diff --git a/tests/scripts/traefik_install_test.sh b/tests/scripts/traefik_install_test.sh new file mode 100644 index 0000000..18aa812 --- /dev/null +++ b/tests/scripts/traefik_install_test.sh @@ -0,0 +1,147 @@ +#!/bin/bash + +# shellcheck source=tests/scripts/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +STUB_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/stub" && pwd)" +export PATH="$STUB_BIN:$PATH" KUBECONFIG=/dev/null + +nginx_ports() { + printf '%s' "$1" >"$WORK_DIR/nginx-http-port" + printf '%s' "$2" >"$WORK_DIR/nginx-https-port" +} + +ports_file() { + printf '%s\n' "$@" >"$WORK_DIR/traefik-config.env" +} + +run_carry_over() { + ( + # shellcheck source=utils/colors.sh + source "$TEST_ROOT/utils/colors.sh" + # shellcheck source=utils/update_env_var.sh + source "$TEST_ROOT/utils/update_env_var.sh" + # shellcheck source=utils/install_traefik.sh + source "$TEST_ROOT/utils/install_traefik.sh" + carry_over_ingress_nginx_ports "$WORK_DIR/traefik-config.env" + ) >"$WORK_DIR/output" 2>&1 || fail "carry_over_ingress_nginx_ports exited $?: $(cat "$WORK_DIR/output")" +} + +port() { + grep "^$1=" "$WORK_DIR/traefik-config.env" | tail -n 1 +} + +test_ingress_nginx_on_other_ports_keeps_them_through_the_switch() { + nginx_ports 8080 8443 + ports_file HTTP_PORT=80 HTTPS_PORT=443 + run_carry_over + assert_eq "$(port HTTP_PORT)" "HTTP_PORT=8080" + assert_eq "$(port HTTPS_PORT)" "HTTPS_PORT=8443" + assert_contains "$(cat "$WORK_DIR/output")" "8080/8443" +} + +test_a_lost_ports_file_is_rebuilt_from_ingress_nginx() { + nginx_ports 8080 8443 + run_carry_over + assert_eq "$(port HTTP_PORT)" "HTTP_PORT=8080" + assert_eq "$(port HTTPS_PORT)" "HTTPS_PORT=8443" +} + +test_ports_set_in_the_file_win() { + nginx_ports 8080 8443 + ports_file HTTP_PORT=9080 HTTPS_PORT=9443 + run_carry_over + assert_eq "$(port HTTP_PORT)" "HTTP_PORT=9080" + assert_eq "$(port HTTPS_PORT)" "HTTPS_PORT=9443" +} + +test_ingress_nginx_on_the_defaults_changes_nothing() { + nginx_ports 80 443 + ports_file HTTP_PORT=80 HTTPS_PORT=443 + run_carry_over + assert_eq "$(cat "$WORK_DIR/traefik-config.env")" "$(printf 'HTTP_PORT=80\nHTTPS_PORT=443')" + assert_eq "$(cat "$WORK_DIR/output")" "" +} + +test_an_install_without_ingress_nginx_changes_nothing() { + ports_file HTTP_PORT=80 HTTPS_PORT=443 + run_carry_over + assert_eq "$(cat "$WORK_DIR/traefik-config.env")" "$(printf 'HTTP_PORT=80\nHTTPS_PORT=443')" +} + +run_install_chart() { + ( + # shellcheck source=utils/colors.sh + source "$TEST_ROOT/utils/colors.sh" + # shellcheck source=utils/output_redirect.sh + source "$TEST_ROOT/utils/output_redirect.sh" + # shellcheck source=utils/install_traefik.sh + source "$TEST_ROOT/utils/install_traefik.sh" + install_helm_chart gateway "$TEST_ROOT/charts/gateway" "" + ) >"$WORK_DIR/output" 2>&1 || fail "install_helm_chart exited $?: $(cat "$WORK_DIR/output")" +} + +calls() { + grep -E ' (patch helmchart|delete job) ' "$WORK_DIR/calls" 2>/dev/null || true +} + +test_a_rerun_after_a_failed_install_tries_it_again() { + touch "$WORK_DIR/job-failed" + echo "Error: context deadline exceeded" >"$WORK_DIR/job-logs" + run_install_chart + assert_eq "$(calls)" "--kubeconfig=/dev/null delete job helm-install-gateway -n kube-system" +} + +test_a_release_waiting_for_intervention_is_reinstalled_on_the_rerun() { + touch "$WORK_DIR/job-failed" + echo "Release status is 'failed' and failure policy is 'abort', not 'reinstall'; waiting for operator intervention" >"$WORK_DIR/job-logs" + run_install_chart + assert_contains "$(calls)" "patch helmchart gateway -n kube-system --type merge -p {\"spec\":{\"failurePolicy\":\"reinstall\"}}" + assert_contains "$(cat "$WORK_DIR/output")" "reinstalling it" +} + +test_an_unchanged_chart_that_installed_is_left_alone() { + run_install_chart + assert_eq "$(calls)" "" +} + +run_retire() { + ( + export TRAEFIK_ADDRESS_TIMEOUT=0 + # shellcheck source=utils/colors.sh + source "$TEST_ROOT/utils/colors.sh" + # shellcheck source=utils/output_redirect.sh + source "$TEST_ROOT/utils/output_redirect.sh" + # shellcheck source=utils/retire_ingress_nginx.sh + source "$TEST_ROOT/utils/retire_ingress_nginx.sh" + retire_ingress_nginx "$@" + ) >"$WORK_DIR/output" 2>&1 +} + +test_a_stalled_handover_gives_ingress_nginx_its_ports_back() { + nginx_ports 8080 8443 + if run_retire; then + fail "retire_ingress_nginx succeeded with no Traefik address" + fi + assert_contains "$(cat "$WORK_DIR/applied")" "name: ingress-nginx-controller" + assert_contains "$(cat "$WORK_DIR/applied")" "port: 8080" + assert_contains "$(cat "$WORK_DIR/applied")" "port: 8443" + assert_contains "$(cat "$WORK_DIR/output")" "keeps serving the panel" +} + +test_a_handover_that_lands_leaves_ingress_nginx_gone() { + nginx_ports 80 443 + echo '[{"ip":"10.0.0.1"}]' >"$WORK_DIR/traefik-address" + run_retire || fail "retire_ingress_nginx failed: $(cat "$WORK_DIR/output")" + [ ! -f "$WORK_DIR/applied" ] || fail "ingress-nginx's Service was put back" + assert_contains "$(cat "$WORK_DIR/calls")" "delete namespace ingress-nginx" +} + +test_dev_retires_without_waiting_for_routes() { + nginx_ports 80 443 + echo '[{"ip":"10.0.0.1"}]' >"$WORK_DIR/traefik-address" + run_retire --immediately || fail "retire_ingress_nginx failed: $(cat "$WORK_DIR/output")" + assert_not_contains "$(cat "$WORK_DIR/calls")" "wait httproute/api" +} + +run_tests diff --git a/utils/install_traefik.sh b/utils/install_traefik.sh index 41e9557..735e193 100644 --- a/utils/install_traefik.sh +++ b/utils/install_traefik.sh @@ -6,6 +6,37 @@ # cannot also create a GatewayClass or Gateway: Helm resolves every kind before # it applies anything. The panel's own Gateway and routes are in the panel # overlay and only need this to have run first. +# An install ingress-nginx still serves can be listening on ports its env file +# doesn't say: the Service edited by hand, or the file lost. Those are the ports +# people reach it on, so Traefik takes them over rather than going back to +# 80/443. Ports set in the file win. +carry_over_ingress_nginx_ports() { + local ports_env="$1" http https live_http live_https + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + live_http="$(kubectl "${kc[@]}" -n ingress-nginx get service ingress-nginx-controller \ + -o jsonpath='{.spec.ports[?(@.name=="http")].port}' 2>/dev/null)" + live_https="$(kubectl "${kc[@]}" -n ingress-nginx get service ingress-nginx-controller \ + -o jsonpath='{.spec.ports[?(@.name=="https")].port}' 2>/dev/null)" + if ! [[ "$live_http" =~ ^[0-9]+$ && "$live_https" =~ ^[0-9]+$ ]]; then + return 0 + fi + + http="$(grep -s '^HTTP_PORT=' "$ports_env" | tail -n 1 | cut -d= -f2- | tr -d '\r" ')" + https="$(grep -s '^HTTPS_PORT=' "$ports_env" | tail -n 1 | cut -d= -f2- | tr -d '\r" ')" + if [ "${http:-80}" != 80 ] || [ "${https:-443}" != 443 ]; then + return 0 + fi + if [ "$live_http" = 80 ] && [ "$live_https" = 443 ]; then + return 0 + fi + + update_env_var "$ports_env" HTTP_PORT "$live_http" + update_env_var "$ports_env" HTTPS_PORT "$live_https" + warn "ingress-nginx was serving on ports $live_http/$live_https, so Traefik keeps them (saved to $ports_env)" +} + install_traefik() { local ports_env="${1:-$PANEL_DIR/overlays/config/traefik-config.env}" local minor http_port https_port crd @@ -20,6 +51,8 @@ install_traefik() { return 1 fi + carry_over_ingress_nginx_ports "$ports_env" + http_port="$(grep -s '^HTTP_PORT=' "$ports_env" | tail -n 1 | cut -d= -f2- | tr -d '\r" ')" https_port="$(grep -s '^HTTPS_PORT=' "$ports_env" | tail -n 1 | cut -d= -f2- | tr -d '\r" ')" http_port="${http_port:-80}" @@ -88,9 +121,29 @@ chart_archive() { # binary) and waits on the helm-install Job that helm-controller runs for it. # That Job keeps its name across changes; a changed HelmChart gets a new Job, # so a Job with the old UID is the previous config's and is not waited on. +# helm-controller never re-runs a failed Job for a HelmChart that hasn't +# changed, so a re-run of update.sh has to start one. A release that failed under +# failurePolicy abort refuses every new attempt until someone intervenes, and +# without a helm binary the only way out is a reinstall: done on the re-run, not +# the run that failed, so a failed upgrade never takes Traefik down on its own. +retry_helm_chart() { + local name="$1" + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + if kubectl "${kc[@]}" logs "job/helm-install-$name" -n kube-system --tail=50 2>/dev/null | + grep -q "waiting for operator intervention"; then + warn "The last $name release failed; reinstalling it, so the panel is offline for a few seconds" + output_redirect kubectl "${kc[@]}" patch helmchart "$name" -n kube-system --type merge \ + -p '{"spec":{"failurePolicy":"reinstall"}}' || return 1 + else + output_redirect kubectl "${kc[@]}" delete job "helm-install-$name" -n kube-system || return 1 + fi +} + install_helm_chart() { local name="$1" dir="$2" values="$3" - local manifest old_generation new_generation old_uid uid complete failed deadline + local manifest old_generation new_generation old_uid uid complete failed deadline last_failed local -a kc=() [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") @@ -122,6 +175,8 @@ install_helm_chart() { old_generation="$(kubectl "${kc[@]}" get helmchart "$name" -n kube-system -o jsonpath='{.metadata.generation}' 2>/dev/null)" old_uid="$(kubectl "${kc[@]}" get job "helm-install-$name" -n kube-system -o jsonpath='{.metadata.uid}' 2>/dev/null)" + last_failed="$(kubectl "${kc[@]}" get job "helm-install-$name" -n kube-system \ + -o jsonpath='{.status.conditions[?(@.type=="Failed")].status}' 2>/dev/null)" # Server-side: chartContent is far past the 256KiB a client-side apply can # keep in its last-applied annotation. @@ -131,7 +186,11 @@ install_helm_chart() { fi new_generation="$(kubectl "${kc[@]}" get helmchart "$name" -n kube-system -o jsonpath='{.metadata.generation}')" if [ "$new_generation" = "$old_generation" ]; then - old_uid="" + if [ "$last_failed" = "True" ]; then + retry_helm_chart "$name" || return 1 + else + old_uid="" + fi fi echo "Waiting for helm-controller to apply $name..." @@ -146,6 +205,7 @@ install_helm_chart() { if [ "$failed" = "True" ]; then err "helm-controller could not apply $name:" kubectl "${kc[@]}" logs "job/helm-install-$name" -n kube-system --tail=30 >&2 + err "Run ./update.sh again to retry it" return 1 fi fi diff --git a/utils/retire_ingress_nginx.sh b/utils/retire_ingress_nginx.sh index a0f404e..1e42e78 100644 --- a/utils/retire_ingress_nginx.sh +++ b/utils/retire_ingress_nginx.sh @@ -13,8 +13,42 @@ PANEL_INGRESSES=(api demos ws relay file-manager hasura web s3-console typesense # down for the seconds in between. The old Ingresses go only after that: until # then ingress-nginx is what serves them, and Traefik's own copies of them # route to the same backends as the HTTPRoutes. +# Puts ingress-nginx's LoadBalancer Service back as the panel shipped it, on the +# ports it had, so a handover that stalls leaves it serving rather than nothing. +restore_ingress_nginx_service() { + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + output_redirect kubectl "${kc[@]}" apply -f - </dev/null 2>&1; then # Not before Traefik has taken up the panel's routes, or it would be # handed the ports with nothing to serve on them. - if ! output_redirect kubectl "${kc[@]}" wait httproute/api -n 5stack --timeout=120s \ + if [ "${1:-}" != --immediately ] && ! output_redirect kubectl "${kc[@]}" wait httproute/api -n 5stack --timeout=120s \ --for=jsonpath='{.status.parents[0].conditions[?(@.type=="Accepted")].status}'=True; then err "Traefik has not accepted the panel's routes; ingress-nginx is left serving" return 1 fi + http_port="$(kubectl "${kc[@]}" get service ingress-nginx-controller -n ingress-nginx \ + -o jsonpath='{.spec.ports[?(@.name=="http")].port}' 2>/dev/null)" + https_port="$(kubectl "${kc[@]}" get service ingress-nginx-controller -n ingress-nginx \ + -o jsonpath='{.spec.ports[?(@.name=="https")].port}' 2>/dev/null)" echo "Moving ports 80/443 from ingress-nginx to Traefik..." output_redirect kubectl "${kc[@]}" delete service ingress-nginx-controller -n ingress-nginx --wait=true + moved=1 fi - deadline=$((SECONDS + 180)) + deadline=$((SECONDS + ${TRAEFIK_ADDRESS_TIMEOUT:-180})) until [ -n "$(kubectl "${kc[@]}" get service traefik -n traefik -o jsonpath='{.status.loadBalancer.ingress}' 2>/dev/null)" ]; do if [ "$SECONDS" -ge "$deadline" ]; then err "Traefik's LoadBalancer Service never got an address:" kubectl "${kc[@]}" get service traefik -n traefik >&2 kubectl "${kc[@]}" get pods -A -l svccontroller.k3s.cattle.io/svcname=traefik >&2 + if [ -n "$moved" ] && restore_ingress_nginx_service "${http_port:-80}" "${https_port:-443}"; then + err "ingress-nginx has its ports back and keeps serving the panel; re-run ./update.sh once Traefik's Service can get an address" + fi return 1 fi sleep 2 From 52bcd0dd39f8fba5d44625600ec3b63c8388f8b0 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 08:57:24 -0400 Subject: [PATCH 08/17] fix: an update takes over the Gateway API CRDs that k3s's removed Traefik left behind, and a failed chart shows helm's error after its pod is gone k3s's traefik-crd release keeps its CRDs when k3s runs with --disable=traefik, still marked as its, so the gateway-crds install was refused. Objects whose release is gone are handed to gateway-crds; a live release's, or ones made outside Helm, stop the install with what to do. The helm-install Job deletes its pod when it gives up, so its error is read while the pod runs, and a re-run picks reinstall from the release's status rather than from logs that are gone. --- tests/gateway/upgrade_test.sh | 20 +++++ tests/scripts/stub/kubectl | 26 ++++++- tests/scripts/traefik_install_test.sh | 108 +++++++++++++++++++++++++- utils/install_traefik.sh | 102 ++++++++++++++++++++++-- 4 files changed, 245 insertions(+), 11 deletions(-) diff --git a/tests/gateway/upgrade_test.sh b/tests/gateway/upgrade_test.sh index 793dbfb..0ab0f7e 100644 --- a/tests/gateway/upgrade_test.sh +++ b/tests/gateway/upgrade_test.sh @@ -33,6 +33,18 @@ install_ingress_nginx() { apply_with_retries "$HERE/fixtures/ingress-nginx-panel.yaml" } +# What k3s's own Traefik leaves once k3s runs with --disable=traefik: the +# Gateway API CRDs, kept by their resource policy and still marked as its +# traefik-crd release, which is gone. +leave_k3s_traefik_crds() { + local -a crds + kubectl apply --server-side -f "$PANEL_DIR/charts/gateway-crds/templates/gateway-api.yaml" >/dev/null || return 1 + kubectl delete validatingadmissionpolicybinding,validatingadmissionpolicy safe-upgrades.gateway.networking.k8s.io >/dev/null || return 1 + mapfile -t crds < <(kubectl get crd -o name | grep '\.gateway\.networking\.k8s\.io$') + kubectl annotate --overwrite "${crds[@]}" meta.helm.sh/release-name=traefik-crd meta.helm.sh/release-namespace=kube-system >/dev/null || return 1 + kubectl label --overwrite "${crds[@]}" app.kubernetes.io/managed-by=Helm >/dev/null +} + # Another app on the same cluster that stays on an nginx-class Ingress: plain # HTTP, its own forward-auth with a sign-in redirect, a body limit, no # buffering and long timeouts. @@ -106,6 +118,12 @@ test_ingress_nginx_is_gone() { [ -z "$(kubectl -n 5stack get ingress -o name)" ] || fail "panel Ingresses left: $(kubectl -n 5stack get ingress -o name)" } +test_the_crds_k3s_left_belong_to_gateway_crds() { + [ "$(kubectl get crd httproutes.gateway.networking.k8s.io -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-name}')" = gateway-crds ] || + fail "httproutes is still marked as another release's" + kubectl get validatingadmissionpolicy safe-upgrades.gateway.networking.k8s.io >/dev/null 2>&1 || fail "the chart's safe-upgrades policy is missing" +} + test_panel_routes_go_through_traefik() { call https "$API" /me expect_code 200 @@ -194,6 +212,7 @@ deploy_panel_stubs || exit 1 tls_secret 5stack-ssl "$DOMAIN" "$API" "demos.$DOMAIN" "ws.$DOMAIN" "tv.$DOMAIN" "search.$DOMAIN" "console.$DOMAIN" || exit 1 tls_secret 5stack-mediamtx-ssl "$HLS" || exit 1 deploy_other_app || exit 1 +leave_k3s_traefik_crds || exit 1 run_test test_before_the_update_ingress_nginx_serves_everything @@ -213,6 +232,7 @@ POLL="$(client cat /tmp/poll.json)" for name in \ test_ingress_nginx_is_gone \ + test_the_crds_k3s_left_belong_to_gateway_crds \ test_panel_routes_go_through_traefik \ test_the_other_app_keeps_its_nginx_ingress_and_auth \ test_the_switch_costs_seconds \ diff --git a/tests/scripts/stub/kubectl b/tests/scripts/stub/kubectl index 064d6ef..3f59315 100755 --- a/tests/scripts/stub/kubectl +++ b/tests/scripts/stub/kubectl @@ -23,10 +23,24 @@ case "$args" in *" apply -f - "*) cat >"$WORK_DIR/applied" ;; *" get service traefik "*) if [ -f "$WORK_DIR/traefik-address" ]; then cat "$WORK_DIR/traefik-address"; fi ;; *" get helmchart "*) echo 1 ;; + *" get customresourcedefinitions,"*) if [ -f "$WORK_DIR/cluster-objects" ]; then cat "$WORK_DIR/cluster-objects"; fi ;; + *" get helmcharts.helm.cattle.io "*) + name="$(printf '%s' "$args" | sed -n 's/.*metadata\.name=\([^ ]*\).*/\1/p')" + if [ -f "$WORK_DIR/helmcharts-$name" ]; then cat "$WORK_DIR/helmcharts-$name"; fi + ;; + *" get secrets "*) + name="$(printf '%s' "$args" | sed -n 's/.*owner=helm,name=\([^ ]*\).*/\1/p')" + if [ -f "$WORK_DIR/releases-$name" ]; then cat "$WORK_DIR/releases-$name"; fi + ;; *" get job helm-install-"*) case "$args" in *'uid}{"|"}'*) - if [ -f "$WORK_DIR/job-retried" ]; then + if [ -f "$WORK_DIR/job-states" ]; then + head -n 1 "$WORK_DIR/job-states" + if [ "$(wc -l <"$WORK_DIR/job-states")" -gt 1 ]; then + sed -i.bak 1d "$WORK_DIR/job-states" + fi + elif [ -f "$WORK_DIR/job-retried" ]; then echo "uid-2|True|" elif [ -f "$WORK_DIR/job-failed" ]; then echo "uid-1||True" @@ -38,7 +52,14 @@ case "$args" in *) echo uid-1 ;; esac ;; - *" logs job/helm-install-"*) if [ -f "$WORK_DIR/job-logs" ]; then cat "$WORK_DIR/job-logs"; fi ;; + *" logs job/helm-install-"*) + if [ ! -f "$WORK_DIR/job-logs" ]; then + echo "error: timed out waiting for the condition" >&2 + exit 1 + fi + cat "$WORK_DIR/job-logs" + ;; + *" logs -n kube-system -l batch.kubernetes.io/controller-uid="*) if [ -f "$WORK_DIR/pod-logs" ]; then cat "$WORK_DIR/pod-logs"; fi ;; *" patch helmchart "* | *" delete job helm-install-"*) touch "$WORK_DIR/job-retried" ;; *" get service ingress-nginx-controller "*) case "$args" in @@ -56,6 +77,7 @@ case "$args" in if [ -f "$WORK_DIR/nodes.next" ]; then mv "$WORK_DIR/nodes.next" "$WORK_DIR/nodes"; fi ;; *" annotate "*) if [ -f "$WORK_DIR/annotate-fails" ]; then exit 1; fi ;; + *" label "*) ;; *" get pods "*) cat "$WORK_DIR/pods" ;; *" get pod "*) cat "$WORK_DIR/containers-$pod" ;; *" logs "*) diff --git a/tests/scripts/traefik_install_test.sh b/tests/scripts/traefik_install_test.sh index 18aa812..c6941a0 100644 --- a/tests/scripts/traefik_install_test.sh +++ b/tests/scripts/traefik_install_test.sh @@ -69,7 +69,7 @@ test_an_install_without_ingress_nginx_changes_nothing() { assert_eq "$(cat "$WORK_DIR/traefik-config.env")" "$(printf 'HTTP_PORT=80\nHTTPS_PORT=443')" } -run_install_chart() { +install_chart() { ( # shellcheck source=utils/colors.sh source "$TEST_ROOT/utils/colors.sh" @@ -78,7 +78,11 @@ run_install_chart() { # shellcheck source=utils/install_traefik.sh source "$TEST_ROOT/utils/install_traefik.sh" install_helm_chart gateway "$TEST_ROOT/charts/gateway" "" - ) >"$WORK_DIR/output" 2>&1 || fail "install_helm_chart exited $?: $(cat "$WORK_DIR/output")" + ) >"$WORK_DIR/output" 2>&1 +} + +run_install_chart() { + install_chart || fail "install_helm_chart exited $?: $(cat "$WORK_DIR/output")" } calls() { @@ -92,14 +96,112 @@ test_a_rerun_after_a_failed_install_tries_it_again() { assert_eq "$(calls)" "--kubeconfig=/dev/null delete job helm-install-gateway -n kube-system" } +# By the time update.sh runs again the failed Job's pod, and its logs, are gone; +# the release records say what state helm left it in. test_a_release_waiting_for_intervention_is_reinstalled_on_the_rerun() { touch "$WORK_DIR/job-failed" - echo "Release status is 'failed' and failure policy is 'abort', not 'reinstall'; waiting for operator intervention" >"$WORK_DIR/job-logs" + printf '%s\n' "9 deployed" "10 failed" >"$WORK_DIR/releases-gateway" run_install_chart assert_contains "$(calls)" "patch helmchart gateway -n kube-system --type merge -p {\"spec\":{\"failurePolicy\":\"reinstall\"}}" assert_contains "$(cat "$WORK_DIR/output")" "reinstalling it" } +test_a_failed_job_on_a_deployed_release_is_just_run_again() { + touch "$WORK_DIR/job-failed" + printf '%s\n' "1 superseded" "2 deployed" >"$WORK_DIR/releases-gateway" + run_install_chart + assert_eq "$(calls)" "--kubeconfig=/dev/null delete job helm-install-gateway -n kube-system" +} + +test_a_failed_install_shows_helms_error_from_before_its_pod_went() { + printf '%s\n' "uid-1||" "uid-1||True" >"$WORK_DIR/job-states" + echo 'Error: INSTALLATION FAILED: Unable to continue with install: CustomResourceDefinition "httproutes.gateway.networking.k8s.io" exists' >"$WORK_DIR/pod-logs" + if install_chart; then + fail "install_helm_chart succeeded for a failed Job" + fi + assert_contains "$(cat "$WORK_DIR/output")" "Unable to continue with install" + assert_not_contains "$(cat "$WORK_DIR/output")" "timed out waiting for the condition" +} + +test_a_failed_install_whose_pod_left_nothing_says_so() { + echo "uid-1||True" >"$WORK_DIR/job-states" + if install_chart; then + fail "install_helm_chart succeeded for a failed Job" + fi + assert_contains "$(cat "$WORK_DIR/output")" "its pod is already gone" +} + +cluster_objects() { + printf '%s\n' "$@" >"$WORK_DIR/cluster-objects" +} + +hand_over() { + ( + # shellcheck source=utils/colors.sh + source "$TEST_ROOT/utils/colors.sh" + # shellcheck source=utils/output_redirect.sh + source "$TEST_ROOT/utils/output_redirect.sh" + # shellcheck source=utils/install_traefik.sh + source "$TEST_ROOT/utils/install_traefik.sh" + adopt_orphaned_chart_objects "$TEST_ROOT/charts/gateway-crds" gateway-crds traefik + ) >"$WORK_DIR/output" 2>&1 +} + +handovers() { + grep -E ' (annotate|label) ' "$WORK_DIR/calls" 2>/dev/null || true +} + +K3S_HTTPROUTES="CustomResourceDefinition/httproutes.gateway.networking.k8s.io|traefik-crd|kube-system" +K3S_GATEWAYS="CustomResourceDefinition/gateways.gateway.networking.k8s.io|traefik-crd|kube-system" + +test_crds_left_by_k3s_traefik_are_handed_to_gateway_crds() { + cluster_objects "$K3S_HTTPROUTES" "$K3S_GATEWAYS" \ + "CustomResourceDefinition/gatewayclasses.gateway.networking.k8s.io|gateway-crds|traefik" \ + "CustomResourceDefinition/helmcharts.helm.cattle.io||" + hand_over || fail "adopt_orphaned_chart_objects failed: $(cat "$WORK_DIR/output")" + assert_contains "$(handovers)" "annotate --overwrite customresourcedefinition/httproutes.gateway.networking.k8s.io customresourcedefinition/gateways.gateway.networking.k8s.io meta.helm.sh/release-name=gateway-crds meta.helm.sh/release-namespace=traefik" + assert_contains "$(handovers)" "label --overwrite customresourcedefinition/httproutes.gateway.networking.k8s.io customresourcedefinition/gateways.gateway.networking.k8s.io app.kubernetes.io/managed-by=Helm" + assert_not_contains "$(handovers)" gatewayclasses + assert_not_contains "$(handovers)" helmcharts +} + +test_k3s_still_running_its_traefik_stops_the_install() { + cluster_objects "$K3S_HTTPROUTES" + echo helmchart.helm.cattle.io/traefik-crd >"$WORK_DIR/helmcharts-traefik-crd" + if hand_over; then + fail "the CRDs of k3s's running Traefik were taken" + fi + assert_contains "$(cat "$WORK_DIR/output")" "--disable=traefik" + assert_eq "$(handovers)" "" +} + +test_crds_of_another_live_release_stop_the_install() { + cluster_objects "CustomResourceDefinition/httproutes.gateway.networking.k8s.io|eg|envoy-gateway-system" + echo secret/sh.helm.release.v1.eg.v1 >"$WORK_DIR/releases-eg" + if hand_over; then + fail "another release's CRDs were taken" + fi + assert_contains "$(cat "$WORK_DIR/output")" "belongs to the Helm release eg in envoy-gateway-system" + assert_eq "$(handovers)" "" +} + +test_crds_created_outside_helm_stop_the_install() { + cluster_objects "CustomResourceDefinition/httproutes.gateway.networking.k8s.io||" + if hand_over; then + fail "CRDs created outside Helm were taken" + fi + assert_contains "$(cat "$WORK_DIR/output")" "created outside Helm" + assert_eq "$(handovers)" "" +} + +test_a_cluster_with_nothing_left_over_hands_nothing_over() { + cluster_objects "CustomResourceDefinition/helmcharts.helm.cattle.io||" \ + "CustomResourceDefinition/httproutes.gateway.networking.k8s.io|gateway-crds|traefik" \ + "ValidatingAdmissionPolicy/safe-upgrades.gateway.networking.k8s.io|gateway-crds|traefik" + hand_over || fail "adopt_orphaned_chart_objects failed: $(cat "$WORK_DIR/output")" + assert_eq "$(handovers)" "" +} + test_an_unchanged_chart_that_installed_is_left_alone() { run_install_chart assert_eq "$(calls)" "" diff --git a/utils/install_traefik.sh b/utils/install_traefik.sh index 735e193..2a7668e 100644 --- a/utils/install_traefik.sh +++ b/utils/install_traefik.sh @@ -62,6 +62,7 @@ install_traefik() { return 1 fi + adopt_orphaned_chart_objects "$PANEL_DIR/charts/gateway-crds" gateway-crds traefik || return 1 install_helm_chart gateway-crds "$PANEL_DIR/charts/gateway-crds" "" || return 1 for crd in gatewayclasses.gateway.networking.k8s.io gateways.gateway.networking.k8s.io \ @@ -103,6 +104,76 @@ EOF fi } +# The cluster-scoped objects a chart's templates define, as kind/name. +chart_objects() { + awk '/^kind: / { kind = $2; next } + kind && /^ name: / { name = $2; gsub(/"/, "", name); print tolower(kind) "/" name; kind = "" }' "$1"/templates/*.yaml +} + +helm_release_gone() { + local charts records + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + charts="$(kubectl "${kc[@]}" get helmcharts.helm.cattle.io -A --field-selector "metadata.name=$1" -o name)" || return 1 + records="$(kubectl "${kc[@]}" -n "$2" get secrets -l "owner=helm,name=$1" -o name)" || return 1 + [ -z "$charts$records" ] +} + +# k3s's bundled Traefik installs the Gateway API CRDs as its traefik-crd +# release with helm.sh/resource-policy: keep, so turning it off with +# --disable=traefik deletes the release but leaves the CRDs marked as its, and +# Helm won't install over objects another release claims. Ones whose release +# is gone are handed to this one; anything a live release, or no release, owns +# stops the install. +adopt_orphaned_chart_objects() { + local chart_dir="$1" release="$2" release_namespace="$3" + local objects existing object owner owner_namespace refused=false + local -a kc=() orphaned=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + objects="$(chart_objects "$chart_dir")" + if ! existing="$(kubectl "${kc[@]}" get customresourcedefinitions,validatingadmissionpolicies,validatingadmissionpolicybindings \ + -o jsonpath='{range .items[*]}{.kind}/{.metadata.name}|{.metadata.annotations.meta\.helm\.sh/release-name}|{.metadata.annotations.meta\.helm\.sh/release-namespace}{"\n"}{end}')"; then + err "could not read the cluster's CRDs before installing $release" + return 1 + fi + + while IFS='|' read -r object owner owner_namespace; do + [ -n "$object" ] || continue + object="$(printf '%s' "$object" | tr '[:upper:]' '[:lower:]')" + grep -qxF "$object" <<<"$objects" || continue + if [ "$owner" = "$release" ] && [ "$owner_namespace" = "$release_namespace" ]; then + continue + fi + if [ -z "$owner" ]; then + err "$object was created outside Helm, so $release can't install it; delete it, or label it app.kubernetes.io/managed-by=Helm and annotate it meta.helm.sh/release-name=$release meta.helm.sh/release-namespace=$release_namespace" + refused=true + elif ! helm_release_gone "$owner" "$owner_namespace"; then + if [ "$owner" = traefik-crd ]; then + err "k3s's bundled Traefik owns $object; add --disable=traefik to k3s's server flags, restart k3s, and run this again" + else + err "$object belongs to the Helm release $owner in $owner_namespace, so $release can't install it; uninstall that release first" + fi + refused=true + else + orphaned+=("$object") + fi + done <<<"$existing" + + if [ "$refused" = true ]; then + return 1 + fi + if [ "${#orphaned[@]}" -eq 0 ]; then + return 0 + fi + + warn "Handing ${#orphaned[@]} objects left by a removed Helm release to $release" + output_redirect kubectl "${kc[@]}" annotate --overwrite "${orphaned[@]}" \ + "meta.helm.sh/release-name=$release" "meta.helm.sh/release-namespace=$release_namespace" || return 1 + output_redirect kubectl "${kc[@]}" label --overwrite "${orphaned[@]}" app.kubernetes.io/managed-by=Helm +} + # A chart directory as the .tgz Helm expects, byte-identical between runs where # GNU tar allows it, so an unchanged chart is not reinstalled. chart_archive() { @@ -126,13 +197,17 @@ chart_archive() { # failurePolicy abort refuses every new attempt until someone intervenes, and # without a helm binary the only way out is a reinstall: done on the re-run, not # the run that failed, so a failed upgrade never takes Traefik down on its own. +# helm-controller turns a release left pending (other than an upgrade, which it +# retries) into a failed one before it gives up on it. retry_helm_chart() { - local name="$1" + local name="$1" status local -a kc=() [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") - if kubectl "${kc[@]}" logs "job/helm-install-$name" -n kube-system --tail=50 2>/dev/null | - grep -q "waiting for operator intervention"; then + status="$(kubectl "${kc[@]}" -n traefik get secrets -l "owner=helm,name=$name" \ + -o jsonpath='{range .items[*]}{.metadata.labels.version}{" "}{.metadata.labels.status}{"\n"}{end}' 2>/dev/null | + sort -n | tail -n 1 | cut -d' ' -f2)" + if [[ "$status" =~ ^(failed|pending-install|pending-rollback|uninstalling)$ ]]; then warn "The last $name release failed; reinstalling it, so the panel is offline for a few seconds" output_redirect kubectl "${kc[@]}" patch helmchart "$name" -n kube-system --type merge \ -p '{"spec":{"failurePolicy":"reinstall"}}' || return 1 @@ -143,7 +218,7 @@ retry_helm_chart() { install_helm_chart() { local name="$1" dir="$2" values="$3" - local manifest old_generation new_generation old_uid uid complete failed deadline last_failed + local manifest old_generation new_generation old_uid uid complete failed deadline last_failed logs helm_error="" local -a kc=() [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") @@ -204,16 +279,31 @@ install_helm_chart() { fi if [ "$failed" = "True" ]; then err "helm-controller could not apply $name:" - kubectl "${kc[@]}" logs "job/helm-install-$name" -n kube-system --tail=30 >&2 + show_helm_error "$name" "$helm_error" err "Run ./update.sh again to retry it" return 1 fi + # The Job restarts helm inside its pod and deletes the pod when it + # gives up, so helm's error is read while the pod is still there. + logs="$(kubectl "${kc[@]}" logs -n kube-system -l "batch.kubernetes.io/controller-uid=$uid" --previous --tail=30 2>/dev/null)" + [ -n "$logs" ] && helm_error="$logs" fi if [ "$SECONDS" -ge "$deadline" ]; then err "helm-controller did not apply $name in time:" - kubectl "${kc[@]}" logs "job/helm-install-$name" -n kube-system --tail=30 >&2 + show_helm_error "$name" "$helm_error" return 1 fi sleep 3 done } + +show_helm_error() { + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + if [ -n "$2" ]; then + printf '%s\n' "$2" >&2 + elif ! kubectl "${kc[@]}" logs "job/helm-install-$1" -n kube-system --tail=30 --pod-running-timeout=5s >&2 2>/dev/null; then + err "helm-install-$1 left no logs: its pod is already gone" + fi +} From 1d8acd0f78aadf5833a70f47d84ba8201ecc1ce0 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 09:03:11 -0400 Subject: [PATCH 09/17] fix: the leftover-CRD check only runs before gateway-crds is first installed, so a second update isn't stopped by the traefik.io CRDs Traefik's chart re-creates without Helm's marks --- tests/scripts/traefik_install_test.sh | 17 +++++++++++++++++ utils/install_traefik.sh | 16 +++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/scripts/traefik_install_test.sh b/tests/scripts/traefik_install_test.sh index c6941a0..a0c28b7 100644 --- a/tests/scripts/traefik_install_test.sh +++ b/tests/scripts/traefik_install_test.sh @@ -194,6 +194,23 @@ test_crds_created_outside_helm_stop_the_install() { assert_eq "$(handovers)" "" } +# Traefik's own chart re-creates the traefik.io CRDs without Helm's marks once +# gateway-crds is installed; an update after that must not trip over them. +test_an_installed_release_is_left_to_helm() { + cluster_objects "CustomResourceDefinition/ingressroutes.traefik.io||" \ + "CustomResourceDefinition/httproutes.gateway.networking.k8s.io|gateway-crds|traefik" + echo secret/sh.helm.release.v1.gateway-crds.v1 >"$WORK_DIR/releases-gateway-crds" + hand_over || fail "adopt_orphaned_chart_objects failed: $(cat "$WORK_DIR/output")" + assert_eq "$(handovers)" "" +} + +test_a_first_install_that_failed_still_hands_k3s_crds_over() { + cluster_objects "$K3S_HTTPROUTES" + echo helmchart.helm.cattle.io/gateway-crds >"$WORK_DIR/helmcharts-gateway-crds" + hand_over || fail "adopt_orphaned_chart_objects failed: $(cat "$WORK_DIR/output")" + assert_contains "$(handovers)" "annotate --overwrite customresourcedefinition/httproutes.gateway.networking.k8s.io" +} + test_a_cluster_with_nothing_left_over_hands_nothing_over() { cluster_objects "CustomResourceDefinition/helmcharts.helm.cattle.io||" \ "CustomResourceDefinition/httproutes.gateway.networking.k8s.io|gateway-crds|traefik" \ diff --git a/utils/install_traefik.sh b/utils/install_traefik.sh index 2a7668e..c8b22e3 100644 --- a/utils/install_traefik.sh +++ b/utils/install_traefik.sh @@ -120,18 +120,32 @@ helm_release_gone() { [ -z "$charts$records" ] } +helm_release_recorded() { + local records + local -a kc=() + [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + + records="$(kubectl "${kc[@]}" -n "$2" get secrets -l "owner=helm,name=$1" -o name 2>/dev/null)" && [ -n "$records" ] +} + # k3s's bundled Traefik installs the Gateway API CRDs as its traefik-crd # release with helm.sh/resource-policy: keep, so turning it off with # --disable=traefik deletes the release but leaves the CRDs marked as its, and # Helm won't install over objects another release claims. Ones whose release # is gone are handed to this one; anything a live release, or no release, owns -# stops the install. +# stops the install. Only a first install can collide: Helm upgrades what a +# release already holds whatever its marks say, and the Traefik chart's own +# crds/ strips them from the traefik.io CRDs the two charts share. adopt_orphaned_chart_objects() { local chart_dir="$1" release="$2" release_namespace="$3" local objects existing object owner owner_namespace refused=false local -a kc=() orphaned=() [ -n "${KUBECONFIG:-}" ] && kc=(--kubeconfig="$KUBECONFIG") + if helm_release_recorded "$release" "$release_namespace"; then + return 0 + fi + objects="$(chart_objects "$chart_dir")" if ! existing="$(kubectl "${kc[@]}" get customresourcedefinitions,validatingadmissionpolicies,validatingadmissionpolicybindings \ -o jsonpath='{range .items[*]}{.kind}/{.metadata.name}|{.metadata.annotations.meta\.helm\.sh/release-name}|{.metadata.annotations.meta\.helm\.sh/release-namespace}{"\n"}{end}')"; then From 37baceea4cbe3fa6e769bc6566734d970c5c2831 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 09:16:00 -0400 Subject: [PATCH 10/17] fix: a reverse-proxy install answers its hosts on 443 too, as ingress-nginx did, so a proxy forwarding there keeps working and logins see HTTPS Proxies that forward to the node over HTTPS got Traefik's 404 for every panel host, because a REVERSE_PROXY install's listeners were plain HTTP only. A second Gateway, 5stack-tls, serves the same hosts on websecure with a self-signed certificate write_overlays makes once and keeps, and every route attaches to both. Behind such a proxy the api now sees X-Forwarded-Proto: https, which its Secure session cookie needs. Port 80 is unchanged. An https install removes the Gateway and its certificate. --- .github/workflows/panel-scripts.yml | 1 + overlays/http/gateway-tls.yaml | 78 ++++++++++++++++++++++++++++ overlays/http/http-replacements.yaml | 40 ++++++++++++++ overlays/http/reverse-proxy-tls.yaml | 21 ++++++++ tests/gateway/fresh_install_test.sh | 29 +++++++++++ tests/gateway/lib.sh | 3 +- tests/scripts/write_overlays_test.sh | 63 ++++++++++++++++++++++ update.sh | 7 ++- utils/write_overlays.sh | 25 ++++++++- 9 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 overlays/http/gateway-tls.yaml create mode 100644 overlays/http/reverse-proxy-tls.yaml create mode 100644 tests/scripts/write_overlays_test.sh diff --git a/.github/workflows/panel-scripts.yml b/.github/workflows/panel-scripts.yml index eaf0ba5..b332457 100644 --- a/.github/workflows/panel-scripts.yml +++ b/.github/workflows/panel-scripts.yml @@ -46,4 +46,5 @@ jobs: - run: bash tests/scripts/plugin_test.sh - run: bash tests/scripts/node_ip_test.sh - run: bash tests/scripts/traefik_install_test.sh + - run: bash tests/scripts/write_overlays_test.sh - run: bash tests/scripts/dev_game_server_test.sh diff --git a/overlays/http/gateway-tls.yaml b/overlays/http/gateway-tls.yaml new file mode 100644 index 0000000..f91d1f4 --- /dev/null +++ b/overlays/http/gateway-tls.yaml @@ -0,0 +1,78 @@ +# A REVERSE_PROXY install's hosts over HTTPS as well as plain HTTP. Some +# proxies forward to the node on 443 without checking its certificate, which +# ingress-nginx answered for every host with its own self-signed one; this +# Gateway does the same on a certificate write_overlays makes once per install. +# Its listeners are named like the 5stack Gateway's, so reverse-proxy-tls.yaml +# can attach every route to both without knowing which host it is for. +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: 5stack-tls + namespace: 5stack +spec: + gatewayClassName: traefik + listeners: + - name: web + hostname: ${WEB_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-reverse-proxy-tls + - name: api + hostname: ${API_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-reverse-proxy-tls + - name: demos + hostname: ${DEMOS_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-reverse-proxy-tls + - name: ws + hostname: ${WS_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-reverse-proxy-tls + - name: relay + hostname: ${RELAY_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-reverse-proxy-tls + - name: typesense + hostname: ${TYPESENSE_HOST} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-reverse-proxy-tls + - name: s3-console + hostname: ${S3_CONSOLE_HOST} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-reverse-proxy-tls + - name: game-stream + hostname: ${GAME_STREAM_DOMAIN} + protocol: HTTPS + port: 8443 + tls: + mode: Terminate + certificateRefs: + - name: 5stack-reverse-proxy-tls diff --git a/overlays/http/http-replacements.yaml b/overlays/http/http-replacements.yaml index 13568ad..2ef06e8 100644 --- a/overlays/http/http-replacements.yaml +++ b/overlays/http/http-replacements.yaml @@ -9,6 +9,11 @@ replacements: name: 5stack fieldPaths: - spec.listeners.[name=web].hostname + - select: + kind: Gateway + name: 5stack-tls + fieldPaths: + - spec.listeners.[name=web].hostname - select: kind: Middleware name: mediamtx-cors @@ -27,6 +32,11 @@ replacements: name: 5stack fieldPaths: - spec.listeners.[name=api].hostname + - select: + kind: Gateway + name: 5stack-tls + fieldPaths: + - spec.listeners.[name=api].hostname - source: kind: ConfigMap name: api-config @@ -37,6 +47,11 @@ replacements: name: 5stack fieldPaths: - spec.listeners.[name=demos].hostname + - select: + kind: Gateway + name: 5stack-tls + fieldPaths: + - spec.listeners.[name=demos].hostname - source: kind: ConfigMap name: typesense-config @@ -47,6 +62,11 @@ replacements: name: 5stack fieldPaths: - spec.listeners.[name=typesense].hostname + - select: + kind: Gateway + name: 5stack-tls + fieldPaths: + - spec.listeners.[name=typesense].hostname - source: kind: ConfigMap name: s3-config @@ -57,6 +77,11 @@ replacements: name: 5stack fieldPaths: - spec.listeners.[name=s3-console].hostname + - select: + kind: Gateway + name: 5stack-tls + fieldPaths: + - spec.listeners.[name=s3-console].hostname - source: kind: ConfigMap name: api-config @@ -67,6 +92,11 @@ replacements: name: 5stack fieldPaths: - spec.listeners.[name=ws].hostname + - select: + kind: Gateway + name: 5stack-tls + fieldPaths: + - spec.listeners.[name=ws].hostname - source: kind: ConfigMap name: api-config @@ -77,6 +107,11 @@ replacements: name: 5stack fieldPaths: - spec.listeners.[name=relay].hostname + - select: + kind: Gateway + name: 5stack-tls + fieldPaths: + - spec.listeners.[name=relay].hostname - source: kind: ConfigMap name: mediamtx-env @@ -87,6 +122,11 @@ replacements: name: 5stack fieldPaths: - spec.listeners.[name=game-stream].hostname + - select: + kind: Gateway + name: 5stack-tls + fieldPaths: + - spec.listeners.[name=game-stream].hostname - source: kind: ConfigMap name: api-config diff --git a/overlays/http/reverse-proxy-tls.yaml b/overlays/http/reverse-proxy-tls.yaml new file mode 100644 index 0000000..e993f31 --- /dev/null +++ b/overlays/http/reverse-proxy-tls.yaml @@ -0,0 +1,21 @@ +secretGenerator: + - name: 5stack-reverse-proxy-tls + namespace: 5stack + type: kubernetes.io/tls + files: + - tls.crt=reverse-proxy-tls.crt + - tls.key=reverse-proxy-tls.key + options: + disableNameSuffixHash: true +patches: + # Every route also attaches to the same-named listener of 5stack-tls. + - target: + group: gateway.networking.k8s.io + kind: HTTPRoute + patch: |- + - op: copy + from: /spec/parentRefs/0 + path: /spec/parentRefs/- + - op: replace + path: /spec/parentRefs/1/name + value: 5stack-tls diff --git a/tests/gateway/fresh_install_test.sh b/tests/gateway/fresh_install_test.sh index 076c035..e6395ab 100644 --- a/tests/gateway/fresh_install_test.sh +++ b/tests/gateway/fresh_install_test.sh @@ -354,6 +354,34 @@ test_http_mode_serves_every_host_without_redirect() { expect_json .host "$DEMOS" } +# A proxy that forwards to the node on 443, as many did to ingress-nginx's +# self-signed certificate. The api's session cookie is Secure and express only +# sets it when X-Forwarded-Proto says https, so that header is what decides +# whether anyone can log in through such a proxy. +test_http_mode_also_answers_on_443_for_a_proxy_that_forwards_there() { + local route parents + wait_for 200 https "$API" /me + expect_json .backend api + expect_json '.headers["x-forwarded-proto"]' https + call https "$WS" /x + expect_code 200 + expect_json .path /ws/x + expect_json '.headers["x-forwarded-proto"]' https + call https "$HLS" /m1/index.m3u8 + expect_code 401 + call https "$HLS" /m1/index.m3u8 -H 'Cookie: session=ok' + expect_code 200 + call https "$DEMOS" '/5stack/demos/a.dem?X-Amz-Signature=x' + expect_json .backend rustfs + for route in $(kubectl -n 5stack get httproute -o name); do + parents="$(kubectl -n 5stack get "$route" -o jsonpath='{range .status.parents[*]}{.parentRef.name}={.conditions[?(@.type=="Accepted")].status} {end}')" + case "$parents" in + *"5stack=True"*"5stack-tls=True"* | *"5stack-tls=True"*"5stack=True"*) ;; + *) fail "$route is not accepted on both Gateways: $parents" ;; + esac + done +} + start_cluster || exit 1 make_panel || exit 1 install_traefik "$PANEL_DIR/overlays/config/traefik-config.env" || exit 1 @@ -405,5 +433,6 @@ run_test test_a_websocket_held_past_60s_stays_open routing_layer http || exit 1 kubectl delete httproute https-redirect -n 5stack >/dev/null run_test test_http_mode_serves_every_host_without_redirect +run_test test_http_mode_also_answers_on_443_for_a_proxy_that_forwards_there summary diff --git a/tests/gateway/lib.sh b/tests/gateway/lib.sh index 470131f..43fd7d5 100644 --- a/tests/gateway/lib.sh +++ b/tests/gateway/lib.sh @@ -128,7 +128,8 @@ routing_layer() { import re, sys docs = open(sys.argv[1]).read().split("\n---\n") kind = lambda d: (re.search(r"(?m)^kind: (\S+)$", d) or [None, None])[1] -routing = [d for d in docs if kind(d) in ("Namespace", "Gateway", "HTTPRoute", "Middleware", "IngressClass")] +routing = [d for d in docs if kind(d) in ("Namespace", "Gateway", "HTTPRoute", "Middleware", "IngressClass") + or (kind(d) == "Secret" and re.search(r"(?m)^ name: 5stack-reverse-proxy-tls$", d))] open(sys.argv[2], "w").write("\n---\n".join(routing) + "\n") open(sys.argv[3], "w").write("\n---\n".join(d for d in docs if kind(d) == "Issuer") + "\n") PY diff --git a/tests/scripts/write_overlays_test.sh b/tests/scripts/write_overlays_test.sh new file mode 100644 index 0000000..0fb36a5 --- /dev/null +++ b/tests/scripts/write_overlays_test.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# shellcheck source=tests/scripts/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +write() { + ( + export PANEL_DIR="$WORK_DIR/panel" + mkdir -p "$PANEL_DIR/overlays" + cp -R "$TEST_ROOT/overlays/http" "$PANEL_DIR/overlays/http" + # shellcheck source=utils/write_overlays.sh + source "$TEST_ROOT/utils/write_overlays.sh" + write_overlays + ) >"$WORK_DIR/output" 2>&1 +} + +overlay() { + printf '%s' "$WORK_DIR/panel/overlays/$1" +} + +test_a_reverse_proxy_overlay_answers_on_443_with_its_own_certificate() { + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + openssl x509 -noout -in "$(overlay local-secrets-http)/reverse-proxy-tls.crt" || fail "no certificate" + openssl pkey -noout -in "$(overlay local-secrets-http)/reverse-proxy-tls.key" || fail "no key" + assert_contains "$(cat "$(overlay local-secrets-http)/kustomization.yaml")" "- gateway-tls.yaml" + assert_contains "$(cat "$(overlay local-secrets-http)/kustomization.yaml")" "name: 5stack-reverse-proxy-tls" + assert_contains "$(cat "$(overlay local-secrets-http)/kustomization.yaml")" "value: 5stack-tls" + [ -f "$(overlay local-secrets-http)/gateway-tls.yaml" ] || fail "gateway-tls.yaml was not copied" +} + +test_the_certificate_is_kept_across_updates() { + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + cp "$(overlay local-secrets-http)/reverse-proxy-tls.crt" "$WORK_DIR/first.crt" + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + cmp -s "$WORK_DIR/first.crt" "$(overlay local-secrets-http)/reverse-proxy-tls.crt" || fail "the certificate changed" + [ "$(grep -c "name: 5stack-reverse-proxy-tls" "$(overlay local-secrets-http)/kustomization.yaml")" = 1 ] || + fail "the kustomization was appended to, not rewritten" +} + +test_the_key_is_readable_only_by_its_owner() { + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + assert_eq "$(find "$(overlay local-secrets-http)/reverse-proxy-tls.key" -perm 600 | wc -l | tr -d ' ')" 1 +} + +test_an_https_install_has_no_reverse_proxy_listener() { + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + assert_not_contains "$(cat "$(overlay local-secrets-https)/kustomization.yaml")" "5stack-tls" + assert_not_contains "$(cat "$(overlay local-secrets-https)/kustomization.yaml")" "gateway-tls.yaml" + [ ! -e "$(overlay local-secrets-https)/reverse-proxy-tls.key" ] || fail "an https overlay got a certificate" +} + +test_without_openssl_the_update_stops() { + mkdir -p "$WORK_DIR/bin" + printf '#!/bin/sh\nexit 1\n' >"$WORK_DIR/bin/openssl" + chmod +x "$WORK_DIR/bin/openssl" + if PATH="$WORK_DIR/bin:$PATH" write; then + fail "write_overlays succeeded without a certificate" + fi + assert_contains "$(cat "$WORK_DIR/output")" "is openssl installed" + [ ! -e "$(overlay local-secrets-http)/reverse-proxy-tls.key" ] || fail "a half-made key was left" +} + +run_tests diff --git a/update.sh b/update.sh index 6f664a6..53d9135 100755 --- a/update.sh +++ b/update.sh @@ -25,6 +25,11 @@ if [ "$REVERSE_PROXY" = true ]; then done # Behind a proxy that already terminated TLS, this would loop forever. kubectl --kubeconfig=$KUBECONFIG delete httproute https-redirect -n 5stack 2>/dev/null +else + # A REVERSE_PROXY install's answer on 443; this install's own listeners serve + # those hosts there now. + kubectl --kubeconfig=$KUBECONFIG delete gateway 5stack-tls -n 5stack 2>/dev/null + kubectl --kubeconfig=$KUBECONFIG delete secret 5stack-reverse-proxy-tls -n 5stack 2>/dev/null fi if [ "$REVERSE_PROXY" != true ]; then @@ -62,7 +67,7 @@ TURN_DOMAIN="$(grep -s '^TURN_DOMAIN=' "$PANEL_DIR/overlays/coturn/coturn.env" | TURN_DOMAIN="${TURN_DOMAIN%\"}" TURN_DOMAIN="${TURN_DOMAIN#\"}" -write_overlays +write_overlays || die "failed to generate the overlays" ok "overlays generated" step "Applying kustomize overlay" diff --git a/utils/write_overlays.sh b/utils/write_overlays.sh index b8eb3a0..79dc3c5 100644 --- a/utils/write_overlays.sh +++ b/utils/write_overlays.sh @@ -1,5 +1,24 @@ #!/bin/bash +# The self-signed certificate 5stack-tls answers on in a REVERSE_PROXY install +# (overlays/http/gateway-tls.yaml). Made once and kept, so an update changes +# nothing; the proxy in front doesn't check it, as it didn't ingress-nginx's. +ensure_reverse_proxy_certificate() { + local dir="$1" + if [ -s "$dir/reverse-proxy-tls.crt" ] && [ -s "$dir/reverse-proxy-tls.key" ]; then + return 0 + fi + ( + umask 077 + openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj "/CN=5stack" \ + -keyout "$dir/reverse-proxy-tls.key" -out "$dir/reverse-proxy-tls.crt" >/dev/null 2>&1 + ) || { + rm -f "$dir/reverse-proxy-tls.key" "$dir/reverse-proxy-tls.crt" + echo "could not make the reverse proxy's upstream certificate; is openssl installed?" >&2 + return 1 + } +} + # Writes overlays/{vault,local-secrets}-{http,https}, of which update.sh applies # the one REVERSE_PROXY and VAULT_MANAGER select. Reads TURN_DOMAIN and # GPU_VENDOR. @@ -36,13 +55,15 @@ resources: - ../$BASE - ../config $STREAMING_RESOURCES -$(if [[ "$PROTOCOL" == "https" ]]; then echo "- ../cert-manager"; echo "- https-redirect.yaml"; fi) +$(if [[ "$PROTOCOL" == "https" ]]; then echo "- ../cert-manager"; echo "- https-redirect.yaml"; else echo "- gateway-tls.yaml"; fi) EOF if [ "$PROTOCOL" = "https" ]; then cp "$PANEL_DIR/overlays/http/gateway-https-patch.yaml" "$PANEL_DIR/overlays/http/https-redirect.yaml" "$OVERLAY/" cat "$PANEL_DIR/overlays/http/https-replacements.yaml" >> "$OVERLAY/kustomization.yaml" else - cat "$PANEL_DIR/overlays/http/http-replacements.yaml" >> "$OVERLAY/kustomization.yaml" + ensure_reverse_proxy_certificate "$OVERLAY" || return 1 + cp "$PANEL_DIR/overlays/http/gateway-tls.yaml" "$OVERLAY/" + cat "$PANEL_DIR/overlays/http/http-replacements.yaml" "$PANEL_DIR/overlays/http/reverse-proxy-tls.yaml" >> "$OVERLAY/kustomization.yaml" fi done done From 836e1ae9dd016c11ff759ec14c2d58336efc6fee Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 09:32:52 -0400 Subject: [PATCH 11/17] fix: the shared Gateway answers on 443 too, so apps on it keep working behind a reverse proxy that forwards there, and both self-signed upstream certificates renew before they expire traefik-gateway gets a websecure listener: HTTPS on every hostname, open to every namespace, on a self-signed certificate the chart makes itself (CodePier installs it without the panel's scripts). A route attaches to web and websecure to answer either way. The panel's own listeners carry their hostnames and certificates, so they keep their hosts. The chart's certificate is kept across upgrades through lookup and rendered back unchanged until it's within 30 days of the expiry its annotation records; the panel's reverse-proxy certificate is likewise replaced only when it's within 30 days of expiring or unreadable. --- base/gateway/gateway.yaml | 5 +- charts/gateway/Chart.yaml | 2 +- .../gateway/templates/gateway-tls-secret.yaml | 39 +++++++ charts/gateway/values.yaml | 15 ++- tests/gateway/fresh_install_test.sh | 110 +++++++++++++++++- tests/scripts/write_overlays_test.sh | 19 +++ utils/write_overlays.sh | 8 +- 7 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 charts/gateway/templates/gateway-tls-secret.yaml diff --git a/base/gateway/gateway.yaml b/base/gateway/gateway.yaml index 2ac2d0f..0816337 100644 --- a/base/gateway/gateway.yaml +++ b/base/gateway/gateway.yaml @@ -2,8 +2,9 @@ # inherit the hostname from. These are the plain-HTTP listeners of a # REVERSE_PROXY install; overlays/http/gateway-https-patch.yaml turns them into # HTTPS ones. Ports are Traefik's entrypoints (charts/gateway), not the node's: -# HTTP_PORT and HTTPS_PORT only move the LoadBalancer Service. The listener for -# every other host is on the chart's shared Gateway, traefik/traefik-gateway. +# HTTP_PORT and HTTPS_PORT only move the LoadBalancer Service. The listeners for +# every other host are on the chart's shared Gateway, traefik/traefik-gateway: +# web (HTTP) and websecure (HTTPS on a self-signed certificate). apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: diff --git a/charts/gateway/Chart.yaml b/charts/gateway/Chart.yaml index 8282ca7..8829f55 100644 --- a/charts/gateway/Chart.yaml +++ b/charts/gateway/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: gateway -description: Traefik serving the Kubernetes Gateway API, with a shared HTTP Gateway every namespace can attach routes to. Needs the gateway-crds chart. +description: Traefik serving the Kubernetes Gateway API, with a shared Gateway every namespace can attach routes to, on HTTP and on HTTPS with a self-signed certificate. Needs the gateway-crds chart. type: application version: 0.1.0 appVersion: v3.7.16 diff --git a/charts/gateway/templates/gateway-tls-secret.yaml b/charts/gateway/templates/gateway-tls-secret.yaml new file mode 100644 index 0000000..5d25783 --- /dev/null +++ b/charts/gateway/templates/gateway-tls-secret.yaml @@ -0,0 +1,39 @@ +{{- /* +The certificate traefik-gateway's websecure listener terminates on. Reverse +proxies forward to the node over HTTPS without checking it, as they did with +ingress-nginx's self-signed one, so it's made here rather than issued: this +chart also installs on clusters with nothing else to provide it. + +An existing one is rendered back exactly as it is, so an upgrade changes +nothing, until it's within 30 days of the expiry recorded in its annotation; +then, or without a cluster to look in (helm template), a new one is made. +*/ -}} +{{- $name := "traefik-gateway-tls" }} +{{- $expiresKey := "gateway.5stack.gg/not-after" }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $name | default dict }} +{{- $data := $existing.data | default dict }} +{{- $expires := index (($existing.metadata | default dict).annotations | default dict) $expiresKey | default "0" }} +{{- $now := now | unixEpoch | atoi }} +{{- $keep := and (hasKey $data "tls.crt") (hasKey $data "tls.key") (gt (atoi $expires) (add $now 2592000)) }} +{{- $crt := "" }} +{{- $key := "" }} +{{- if $keep }} +{{- $crt = index $data "tls.crt" }} +{{- $key = index $data "tls.key" }} +{{- else }} +{{- $cert := genSelfSignedCert "traefik-gateway" nil nil 3650 }} +{{- $crt = $cert.Cert | b64enc }} +{{- $key = $cert.Key | b64enc }} +{{- $expires = add $now (mul 3650 86400) | toString }} +{{- end }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + namespace: {{ .Release.Namespace }} + annotations: + {{ $expiresKey }}: {{ $expires | quote }} +type: kubernetes.io/tls +data: + tls.crt: {{ $crt }} + tls.key: {{ $key }} diff --git a/charts/gateway/values.yaml b/charts/gateway/values.yaml index b721d8f..ccb8c6f 100644 --- a/charts/gateway/values.yaml +++ b/charts/gateway/values.yaml @@ -16,8 +16,11 @@ traefik: enabled: true name: traefik - # The shared listener: plain HTTP on every hostname, open to routes from any - # namespace. Hostname and HTTPS listeners belong on a Gateway of their own. + # The shared listeners, on every hostname and open to routes from any + # namespace: plain HTTP, and HTTPS on a self-signed certificate + # (templates/gateway-tls-secret.yaml) for reverse proxies that forward to 443. + # A route attaches to both to answer either way. Listeners with a hostname and + # a real certificate belong on a Gateway of their own, and win for their host. gateway: enabled: true listeners: @@ -26,6 +29,14 @@ traefik: protocol: HTTP namespacePolicy: from: All + websecure: + port: 8443 + protocol: HTTPS + namespacePolicy: + from: All + mode: Terminate + certificateRefs: + - name: traefik-gateway-tls # For cert-manager's HTTP-01 solver Ingresses. Not the default class, so an # Ingress without one stays unserved, as it was under ingress-nginx. diff --git a/tests/gateway/fresh_install_test.sh b/tests/gateway/fresh_install_test.sh index e6395ab..efa229d 100644 --- a/tests/gateway/fresh_install_test.sh +++ b/tests/gateway/fresh_install_test.sh @@ -290,6 +290,109 @@ EOF expect_code 404 } +# An app on the shared Gateway, attached to both of its listeners, as a +# reverse proxy that forwards to 443 needs. +shared_route_on_both_listeners() { + kubectl apply -f - >/dev/null <<'EOF' +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: other-both + namespace: other +spec: + parentRefs: + - name: traefik-gateway + namespace: traefik + sectionName: web + - name: traefik-gateway + namespace: traefik + sectionName: websecure + hostnames: + - both.test + rules: + - backendRefs: + - name: other + port: 80 +EOF +} + +# The subject of the certificate served on 443 for SNI $1. +served_certificate() { + client sh -c "openssl s_client -connect $NODE_IP:443 -servername $1 /dev/null | openssl x509 -noout -subject -nameopt RFC2253" +} + +test_the_shared_gateway_answers_on_443_as_well() { + shared_route_on_both_listeners + wait_for 200 https both.test /x + expect_json .backend other + expect_json '.headers["x-forwarded-proto"]' https + call http both.test /x + expect_code 200 + expect_json '.headers["x-forwarded-proto"]' http +} + +# The shared listener on 443 has no hostname; the panel's own listeners carry +# theirs, so they keep their hosts and their certificate. +test_panel_hosts_keep_their_certificate_beside_the_shared_listener() { + [ "$(served_certificate "$API")" = "subject=CN=$WEB" ] || fail "$API was served $(served_certificate "$API")" + call https "$API" /me + expect_code 200 + expect_json .backend api +} + +test_http_mode_shares_443_with_the_shared_gateway() { + call https "$API" /me + expect_code 200 + expect_json .backend api + call https both.test /x + expect_code 200 + expect_json .backend other + expect_json '.headers["x-forwarded-proto"]' https +} + +# Has helm-controller upgrade the gateway release, by changing one value. +upgrade_gateway_release() { + local old_uid uid complete + old_uid="$(kubectl -n kube-system get job helm-install-gateway -o jsonpath='{.metadata.uid}')" + kubectl -n kube-system patch helmchart gateway --type merge \ + -p "{\"spec\":{\"set\":{\"traefik.deployment.podAnnotations.upgraded\":\"$1\"}}}" >/dev/null || return 1 + for _ in $(seq 1 100); do + IFS='|' read -r uid complete < <(kubectl -n kube-system get job helm-install-gateway \ + -o jsonpath='{.metadata.uid}{"|"}{.status.conditions[?(@.type=="Complete")].status}{"\n"}' 2>/dev/null) + if [ -n "$uid" ] && [ "$uid" != "$old_uid" ] && [ "$complete" = True ]; then + return 0 + fi + sleep 3 + done + return 1 +} + +shared_certificate() { + kubectl -n traefik get secret traefik-gateway-tls -o jsonpath='{.data.tls\.crt}' +} + +# helm-controller upgrades the release with cluster access, so lookup finds +# the certificate the install made and the upgrade keeps it. +test_an_upgrade_keeps_the_shared_certificate() { + local before + before="$(shared_certificate)" + [ -n "$before" ] || fail "traefik-gateway-tls is missing" + upgrade_gateway_release once || fail "the upgrade never completed" + [ "$(shared_certificate)" = "$before" ] || fail "the upgrade replaced the certificate" +} + +test_an_upgrade_renews_a_shared_certificate_about_to_expire() { + local before expires + before="$(shared_certificate)" + kubectl -n traefik annotate --overwrite secret traefik-gateway-tls \ + "gateway.5stack.gg/not-after=$(($(date +%s) + 86400))" >/dev/null || fail "could not age the certificate" + upgrade_gateway_release twice || fail "the upgrade never completed" + [ "$(shared_certificate)" != "$before" ] || fail "the upgrade kept a certificate a day from expiring" + expires="$(kubectl -n traefik get secret traefik-gateway-tls -o jsonpath='{.metadata.annotations.gateway\.5stack\.gg/not-after}')" + [ "$expires" -gt $(($(date +%s) + 3000 * 86400)) ] || fail "the new certificate expires at $expires" + wait_for 200 https both.test /x +} + start_long_requests() { client sh -c 'head -c 125829120 /dev/zero >/tmp/big' docker exec -d "$PREFIX-client" sh -c "curl -sk --resolve $DEMOS:443:$NODE_IP -X PUT --data-binary @/tmp/big \ @@ -421,7 +524,9 @@ for name in \ test_snapshot_is_rewritten_with_open_cors \ test_http_redirects_to_https_without_a_port \ test_client_ip_reaches_the_backend \ - test_other_namespaces_attach_to_the_shared_listener; do + test_other_namespaces_attach_to_the_shared_listener \ + test_the_shared_gateway_answers_on_443_as_well \ + test_panel_hosts_keep_their_certificate_beside_the_shared_listener; do run_test "$name" done @@ -434,5 +539,8 @@ routing_layer http || exit 1 kubectl delete httproute https-redirect -n 5stack >/dev/null run_test test_http_mode_serves_every_host_without_redirect run_test test_http_mode_also_answers_on_443_for_a_proxy_that_forwards_there +run_test test_http_mode_shares_443_with_the_shared_gateway +run_test test_an_upgrade_keeps_the_shared_certificate +run_test test_an_upgrade_renews_a_shared_certificate_about_to_expire summary diff --git a/tests/scripts/write_overlays_test.sh b/tests/scripts/write_overlays_test.sh index 0fb36a5..c3eed0d 100644 --- a/tests/scripts/write_overlays_test.sh +++ b/tests/scripts/write_overlays_test.sh @@ -37,6 +37,25 @@ test_the_certificate_is_kept_across_updates() { fail "the kustomization was appended to, not rewritten" } +test_a_certificate_near_its_expiry_is_replaced() { + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + openssl req -x509 -newkey rsa:2048 -nodes -days 10 -subj "/CN=5stack" \ + -keyout "$(overlay local-secrets-http)/reverse-proxy-tls.key" \ + -out "$(overlay local-secrets-http)/reverse-proxy-tls.crt" >/dev/null 2>&1 + cp "$(overlay local-secrets-http)/reverse-proxy-tls.crt" "$WORK_DIR/expiring.crt" + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + cmp -s "$WORK_DIR/expiring.crt" "$(overlay local-secrets-http)/reverse-proxy-tls.crt" && fail "the expiring certificate was kept" + openssl x509 -noout -checkend $((3000 * 86400)) -in "$(overlay local-secrets-http)/reverse-proxy-tls.crt" >/dev/null || + fail "the new certificate isn't good for ten years" +} + +test_an_unreadable_certificate_is_replaced() { + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + echo "not a certificate" >"$(overlay local-secrets-http)/reverse-proxy-tls.crt" + write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" + openssl x509 -noout -in "$(overlay local-secrets-http)/reverse-proxy-tls.crt" || fail "it was not replaced" +} + test_the_key_is_readable_only_by_its_owner() { write || fail "write_overlays failed: $(cat "$WORK_DIR/output")" assert_eq "$(find "$(overlay local-secrets-http)/reverse-proxy-tls.key" -perm 600 | wc -l | tr -d ' ')" 1 diff --git a/utils/write_overlays.sh b/utils/write_overlays.sh index 79dc3c5..c8e8cb1 100644 --- a/utils/write_overlays.sh +++ b/utils/write_overlays.sh @@ -1,11 +1,13 @@ #!/bin/bash # The self-signed certificate 5stack-tls answers on in a REVERSE_PROXY install -# (overlays/http/gateway-tls.yaml). Made once and kept, so an update changes -# nothing; the proxy in front doesn't check it, as it didn't ingress-nginx's. +# (overlays/http/gateway-tls.yaml). Kept, so an update changes nothing, until +# it's within 30 days of expiring or unreadable; the proxy in front doesn't +# check it, as it didn't ingress-nginx's. ensure_reverse_proxy_certificate() { local dir="$1" - if [ -s "$dir/reverse-proxy-tls.crt" ] && [ -s "$dir/reverse-proxy-tls.key" ]; then + if [ -s "$dir/reverse-proxy-tls.key" ] && + openssl x509 -noout -checkend 2592000 -in "$dir/reverse-proxy-tls.crt" >/dev/null 2>&1; then return 0 fi ( From 8987a23f7c322e18a8b5de08f873a0a7495f8140 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 09:42:04 -0400 Subject: [PATCH 12/17] test: the renewal test ages the shared certificate the way Helm writes it, server-side as helm, so the upgrade's own apply doesn't conflict over the annotation --- tests/gateway/fresh_install_test.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/gateway/fresh_install_test.sh b/tests/gateway/fresh_install_test.sh index efa229d..e914fba 100644 --- a/tests/gateway/fresh_install_test.sh +++ b/tests/gateway/fresh_install_test.sh @@ -381,11 +381,25 @@ test_an_upgrade_keeps_the_shared_certificate() { [ "$(shared_certificate)" = "$before" ] || fail "the upgrade replaced the certificate" } +# Rewrites the certificate's recorded expiry as Helm itself would have written +# it. helm-controller's Helm applies server-side as "helm", so a change made +# under any other field manager would conflict with the upgrade's own apply. +age_shared_certificate() { + kubectl -n traefik get secret traefik-gateway-tls -o json | + python3 -I -c ' +import json, sys +s = json.load(sys.stdin) +m = s["metadata"] +m["annotations"]["gateway.5stack.gg/not-after"] = sys.argv[1] +print(json.dumps({"apiVersion": "v1", "kind": "Secret", "type": s["type"], "data": s["data"], + "metadata": {k: m[k] for k in ("name", "namespace", "labels", "annotations") if k in m}}))' "$1" | + kubectl apply --server-side --field-manager=helm --force-conflicts -f - >/dev/null +} + test_an_upgrade_renews_a_shared_certificate_about_to_expire() { local before expires before="$(shared_certificate)" - kubectl -n traefik annotate --overwrite secret traefik-gateway-tls \ - "gateway.5stack.gg/not-after=$(($(date +%s) + 86400))" >/dev/null || fail "could not age the certificate" + age_shared_certificate "$(($(date +%s) + 86400))" || fail "could not age the certificate" upgrade_gateway_release twice || fail "the upgrade never completed" [ "$(shared_certificate)" != "$before" ] || fail "the upgrade kept a certificate a day from expiring" expires="$(kubectl -n traefik get secret traefik-gateway-tls -o jsonpath='{.metadata.annotations.gateway\.5stack\.gg/not-after}')" From 2646dd721aaf7696021ba7705c1240d4dec4f644 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 10:06:08 -0400 Subject: [PATCH 13/17] test: cert-manager issues and renews an https install's certificates through Traefik, from an in-cluster Pebble, on the panel's own Issuer and Certificates A new gateway job installs cert-manager as update.sh does and runs Pebble with pebble-challtestsrv resolving every test host to Traefik, so HTTP-01 is validated over port 80 the way Let's Encrypt does it. It starts from an ingress-nginx install's 5stack-ssl, due for renewal under the Issuer that install had, runs the update, and checks the renewal and the stream certificate are Pebble-issued for every host and served on 443, that a further renewal is validated and served again, and that without the solver's router priority the https-redirect route takes the challenge path. --- .github/workflows/gateway.yml | 10 + tests/gateway/cert_manager_test.sh | 356 +++++++++++++++++++ tests/gateway/fixtures/nginx-era-issuer.yaml | 18 + 3 files changed, 384 insertions(+) create mode 100644 tests/gateway/cert_manager_test.sh create mode 100644 tests/gateway/fixtures/nginx-era-issuer.yaml diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml index 159001f..7cf8de2 100644 --- a/.github/workflows/gateway.yml +++ b/.github/workflows/gateway.yml @@ -18,6 +18,8 @@ on: - "overlays/mediamtx/**" - "overlays/mediamtx-https/**" - "overlays/cert-manager/**" + - "overlays/cert-manager-crds/**" + - utils/cert_manager.sh - "overlays/config/*.env.example" - "tests/gateway/**" - .github/workflows/gateway.yml @@ -37,6 +39,8 @@ on: - "overlays/mediamtx/**" - "overlays/mediamtx-https/**" - "overlays/cert-manager/**" + - "overlays/cert-manager-crds/**" + - utils/cert_manager.sh - "overlays/config/*.env.example" - "tests/gateway/**" - .github/workflows/gateway.yml @@ -59,3 +63,9 @@ jobs: steps: - uses: actions/checkout@v4 - run: bash tests/gateway/upgrade_test.sh + + cert-manager: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: bash tests/gateway/cert_manager_test.sh diff --git a/tests/gateway/cert_manager_test.sh b/tests/gateway/cert_manager_test.sh new file mode 100644 index 0000000..94f6624 --- /dev/null +++ b/tests/gateway/cert_manager_test.sh @@ -0,0 +1,356 @@ +#!/bin/bash + +# cert-manager issuing and renewing an https install's certificates through +# Traefik, end to end. cert-manager comes from the manifests update.sh applies; +# the Issuer and Certificates are the overlay's own, with only the ACME server +# swapped for Pebble, Let's Encrypt's test CA, running in the cluster. Pebble +# validates HTTP-01 the way Let's Encrypt does, over port 80 to whatever the +# hostname resolves to, and pebble-challtestsrv resolves every test hostname to +# Traefik -- for cert-manager's own self-check too, through CoreDNS. So a +# certificate is only issued if the solver Ingress cert-manager creates from the +# panel's Issuer wins port 80 over the https-redirect route on the same hosts. +# +# The 5stack-ssl it starts from is an ingress-nginx install's: issued under the +# Issuer that install had, and due for renewal once the update has run. + +# shellcheck source=tests/gateway/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +trap cleanup EXIT + +PEBBLE_IMAGE=ghcr.io/letsencrypt/pebble:2.10.1@sha256:ddf230642b1a584f519f32e347de1b05a6e4c1f6c35c1863b33effeab5f78199 +CHALLTESTSRV_IMAGE=ghcr.io/letsencrypt/pebble-challtestsrv:2.10.1@sha256:12ce21884def456bcf9786542113949e1f19dc7738d2c70e156c2d0c38a1405b +PEBBLE_DIRECTORY=https://pebble.pebble.svc:14000/dir +LETS_ENCRYPT_DIRECTORY=https://acme-v02.api.letsencrypt.org/directory + +API="api.$DOMAIN" +WEB="$DOMAIN" +DEMOS="demos.$DOMAIN" +WS="ws.$DOMAIN" +RELAY="tv.$DOMAIN" +HLS="hls.$DOMAIN" +SEARCH="search.$DOMAIN" +CONSOLE="console.$DOMAIN" + +# As update.sh does it: the vendored manifests, the CRDs, then the webhook. +install_cert_manager() { + # shellcheck source=utils/cert_manager.sh + source "$PANEL_DIR/utils/cert_manager.sh" + apply_overlay overlays/cert-manager-crds || return 1 + for crd in certificates.cert-manager.io issuers.cert-manager.io clusterissuers.cert-manager.io; do + kubectl wait --for=condition=Established "crd/$crd" --timeout=120s >/dev/null || return 1 + done + wait_for_cert_manager >/dev/null +} + +# The https overlay update.sh applies, rendered to $WORK_DIR/overlay.yaml, and +# its Issuer and Certificates to $WORK_DIR/cert-manager.yaml with Let's +# Encrypt's directory replaced by Pebble's and nothing else changed. +render_cert_manager_objects() { + write_overlays || return 1 + "$PANEL_DIR/kustomize" build "$PANEL_DIR/overlays/local-secrets-https" >"$WORK_DIR/overlay.yaml" || return 1 + python3 -I - "$WORK_DIR/overlay.yaml" "$LETS_ENCRYPT_DIRECTORY" "$PEBBLE_DIRECTORY" >"$WORK_DIR/cert-manager.yaml" <<'PY' +import re, sys +docs = open(sys.argv[1]).read().split("\n---\n") +kind = lambda d: (re.search(r"(?m)^kind: (\S+)$", d) or [None, None])[1] +out = [] +for d in docs: + if kind(d) == "Issuer": + d, n = re.subn(r"(?m)^( +)server: " + re.escape(sys.argv[2]) + "$", + lambda m: f"{m[1]}server: {sys.argv[3]}\n{m[1]}skipTLSVerify: true", d) + assert n == 1, "the Issuer's ACME server is not Let's Encrypt's" + out.append(d) + elif kind(d) == "Certificate": + out.append(d) +assert len(out) == 3, f"expected an Issuer and two Certificates, got {len(out)}" +print("\n---\n".join(out)) +PY +} + +# One document of $WORK_DIR/cert-manager.yaml, by kind and name. +cert_manager_object() { + python3 -I - "$WORK_DIR/cert-manager.yaml" "$1" "$2" <<'PY' +import re, sys +for d in open(sys.argv[1]).read().split("\n---\n"): + if re.search(rf"(?m)^kind: {sys.argv[2]}$", d) and re.search(rf"(?m)^ name: {re.escape(sys.argv[3])}$", d): + print(d) +PY +} + +# What an ingress-nginx install leaves: the Issuer it had, the panel's +# Certificate, and the 5stack-ssl that Issuer gave it, here two days from +# expiring. cert-manager's controller is held at zero until the update has run: +# on a real install nothing is due during the update, and a renewal attempted +# here, with no ingress-nginx to serve its solver, would fail and back off for an +# hour. +leave_an_nginx_era_certificate() { + kubectl -n cert-manager scale deployment cert-manager --replicas=0 >/dev/null || return 1 + kubectl -n cert-manager wait --for=delete pod -l app.kubernetes.io/component=controller --timeout=120s >/dev/null 2>&1 + kubectl apply -f "$HERE/fixtures/nginx-era-issuer.yaml" >/dev/null || return 1 + cert_manager_object Certificate 5stack-ssl | kubectl apply -f - >/dev/null || return 1 + tls_secret 5stack-ssl "$WEB" "$API" "$DEMOS" "$SEARCH" "$CONSOLE" "$WS" "$RELAY" || return 1 + kubectl -n 5stack annotate --overwrite secret 5stack-ssl cert-manager.io/issuer-name=5stack-issuer \ + cert-manager.io/issuer-kind=Issuer cert-manager.io/issuer-group=cert-manager.io \ + cert-manager.io/certificate-name=5stack-ssl >/dev/null || return 1 + NGINX_ERA_SERIAL="$(secret_certificate 5stack-ssl serial)" +} + +# Pebble, with pebble-challtestsrv beside it answering every DNS query with +# Traefik's address. Validation goes to port 80, as Let's Encrypt's does, and +# no authorization is reused, so every issuance is validated again. +deploy_pebble() { + local traefik_ip + traefik_ip="$(kubectl -n traefik get service traefik -o jsonpath='{.spec.clusterIP}')" + [ -n "$traefik_ip" ] || return 1 + kubectl apply -f - >/dev/null </dev/null || return 1 + + # cert-manager checks a challenge is reachable before presenting it, over + # cluster DNS; k3s's CoreDNS takes extra zones from coredns-custom. + kubectl -n kube-system create configmap coredns-custom --dry-run=client -o yaml \ + --from-literal=pebble.server="$DOMAIN:53 { + forward . $(kubectl -n pebble get service pebble -o jsonpath='{.spec.clusterIP}'):8053 +}" | kubectl apply -f - >/dev/null || return 1 + kubectl -n kube-system rollout restart deployment/coredns >/dev/null || return 1 + kubectl -n kube-system rollout status deployment/coredns --timeout=120s >/dev/null +} + +# A field of the certificate in Secret $1: issuer, serial, or sans (sorted, +# comma separated). +secret_certificate() { + kubectl -n 5stack get secret "$1" -o jsonpath='{.data.tls\.crt}' | base64 -d | certificate_field "$2" +} + +# The same, for the certificate Traefik serves on 443 for SNI $1. +served_certificate() { + client sh -c "openssl s_client -connect $NODE_IP:443 -servername $1 /dev/null | openssl x509" | + certificate_field "$2" +} + +certificate_field() { + case "$1" in + issuer) openssl x509 -noout -issuer -nameopt RFC2253 | sed 's/^issuer=//' ;; + serial) openssl x509 -noout -serial | sed 's/^serial=//' ;; + sans) openssl x509 -noout -ext subjectAltName | tail -n +2 | tr ',' '\n' | sed 's/^ *DNS://' | sort | paste -sd, - ;; + esac +} + +wait_until_ready() { + if ! kubectl -n 5stack wait --for=condition=Ready "$@" --timeout=300s >/dev/null; then + kubectl -n 5stack get certificates,certificaterequests,orders,challenges >&2 + kubectl -n 5stack get challenges -o jsonpath='{range .items[*]}{.spec.dnsName}: {.status.reason}{"\n"}{end}' >&2 + return 1 + fi +} + +# Until Traefik has picked up the Secret's current certificate for SNI $1. +wait_until_served() { + local want + want="$(secret_certificate "$2" serial)" + for _ in $(seq 1 30); do + [ "$(served_certificate "$1" serial)" = "$want" ] && return 0 + sleep 2 + done + fail "$1 is served $(served_certificate "$1" issuer) serial $(served_certificate "$1" serial), not $2's $want" +} + +expected_sans() { + printf '%s\n' "$@" | sort | paste -sd, - +} + +test_the_update_leaves_the_issuer_with_only_the_panels_solver() { + local solver + solver="$(kubectl -n 5stack get issuer 5stack-issuer -o jsonpath='{.spec.acme.solvers[0].http01.ingress}')" + [ "$(jq -r .ingressClassName <<<"$solver")" = traefik ] || fail "solver: $solver" + [ "$(jq -r '.ingressTemplate.metadata.annotations["traefik.ingress.kubernetes.io/router.entrypoints"]' <<<"$solver")" = web ] || + fail "solver: $solver" + [ "$(jq -r '.ingressTemplate.metadata.annotations["traefik.ingress.kubernetes.io/router.priority"]' <<<"$solver")" = 1000000 ] || + fail "solver: $solver" + [ "$(jq -r 'keys | join(",")' <<<"$solver")" = ingressClassName,ingressTemplate ] || fail "solver: $solver" + kubectl -n 5stack get httproute https-redirect >/dev/null || fail "the https-redirect route is missing" +} + +test_the_nginx_era_certificate_is_renewed_by_pebble_through_traefik() { + [ "$(secret_certificate 5stack-ssl serial)" != "$NGINX_ERA_SERIAL" ] || fail "5stack-ssl still holds the nginx-era certificate" + assert_pebble 5stack-ssl + [ "$(secret_certificate 5stack-ssl sans)" = "$(expected_sans "$WEB" "$API" "$DEMOS" "$SEARCH" "$CONSOLE" "$WS" "$RELAY")" ] || + fail "5stack-ssl covers $(secret_certificate 5stack-ssl sans)" +} + +test_the_panel_hosts_serve_it() { + local host + for host in "$API" "$WEB" "$WS"; do + wait_until_served "$host" 5stack-ssl + done + case "$(served_certificate "$API" issuer)" in + *Pebble*) ;; + *) fail "$API is served a certificate from $(served_certificate "$API" issuer)" ;; + esac +} + +test_the_stream_certificate_is_issued_for_the_hls_host() { + assert_pebble 5stack-mediamtx-ssl + [ "$(secret_certificate 5stack-mediamtx-ssl sans)" = "$HLS" ] || fail "5stack-mediamtx-ssl covers $(secret_certificate 5stack-mediamtx-ssl sans)" + wait_until_served "$HLS" 5stack-mediamtx-ssl +} + +# What cmctl renew does: mark the Certificate as issuing. Pebble reuses no +# authorization, so the renewal is validated through Traefik again. +test_a_renewal_is_validated_and_served_again() { + local before + before="$(secret_certificate 5stack-ssl serial)" + kubectl -n 5stack patch certificate 5stack-ssl --subresource=status --type=json -p "[{\"op\": \"add\", + \"path\": \"/status/conditions/-\", \"value\": {\"type\": \"Issuing\", \"status\": \"True\", + \"reason\": \"ManuallyTriggered\", \"message\": \"Certificate re-issuance manually triggered\", + \"lastTransitionTime\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}]" >/dev/null || fail "could not trigger a renewal" + for _ in $(seq 1 90); do + [ "$(secret_certificate 5stack-ssl serial)" != "$before" ] && break + sleep 2 + done + [ "$(secret_certificate 5stack-ssl serial)" != "$before" ] || fail "5stack-ssl was not renewed" + wait_until_ready certificate/5stack-ssl || fail "5stack-ssl is not Ready after its renewal" + assert_pebble 5stack-ssl + wait_until_served "$API" 5stack-ssl +} + +# The pin: the same Issuer without the solver's router priority. Its challenge +# is presented -- the solver Ingress exists -- but plain HTTP for the panel host +# keeps getting the https-redirect route's 308, so cert-manager never sees it +# reachable. The tests above pass only because the priority is there. +test_without_the_solver_priority_the_redirect_takes_the_challenge() { + local token="" code + cert_manager_object Issuer 5stack-issuer | python3 -I -c ' +import sys +d = sys.stdin.read() +assert "router.priority" in d +d = "\n".join(l for l in d.split("\n") if "router.priority" not in l) +print(d.replace("name: 5stack-issuer", "name: no-priority"))' | kubectl apply -f - >/dev/null || fail "could not make the Issuer" + kubectl apply -f - >/dev/null </dev/null 2>&1 && fail "a certificate was issued without the priority" + kubectl -n 5stack delete certificate no-priority >/dev/null + kubectl -n 5stack delete issuer no-priority >/dev/null +} + +assert_pebble() { + case "$(secret_certificate "$1" issuer)" in + *Pebble*) ;; + *) fail "$1 was issued by $(secret_certificate "$1" issuer)" ;; + esac +} + +start_cluster || exit 1 +make_panel || exit 1 +install_cert_manager || exit 1 +kubectl create namespace 5stack >/dev/null || exit 1 +render_cert_manager_objects || exit 1 +leave_an_nginx_era_certificate || exit 1 + +# The update: Traefik, the overlay's Gateway and routes, the Issuer and +# Certificates as they are now. Then cert-manager runs again. +install_traefik "$PANEL_DIR/overlays/config/traefik-config.env" || exit 1 +deploy_pebble || exit 1 +routing_layer https || exit 1 +kubectl apply -f "$WORK_DIR/cert-manager.yaml" >/dev/null || exit 1 +kubectl -n cert-manager scale deployment cert-manager --replicas=1 >/dev/null || exit 1 +kubectl -n cert-manager rollout status deployment/cert-manager --timeout=180s >/dev/null || exit 1 +# The nginx-era certificate still counts as Ready until its renewal lands. +for _ in $(seq 1 150); do + [ "$(secret_certificate 5stack-ssl serial 2>/dev/null)" != "$NGINX_ERA_SERIAL" ] && break + sleep 2 +done +wait_until_ready certificate/5stack-ssl certificate/5stack-mediamtx-ssl || exit 1 + +for name in \ + test_the_update_leaves_the_issuer_with_only_the_panels_solver \ + test_the_nginx_era_certificate_is_renewed_by_pebble_through_traefik \ + test_the_panel_hosts_serve_it \ + test_the_stream_certificate_is_issued_for_the_hls_host \ + test_a_renewal_is_validated_and_served_again \ + test_without_the_solver_priority_the_redirect_takes_the_challenge; do + run_test "$name" +done + +summary diff --git a/tests/gateway/fixtures/nginx-era-issuer.yaml b/tests/gateway/fixtures/nginx-era-issuer.yaml new file mode 100644 index 0000000..f14ebe6 --- /dev/null +++ b/tests/gateway/fixtures/nginx-era-issuer.yaml @@ -0,0 +1,18 @@ +# overlays/cert-manager/issuer.yaml as ingress-nginx installs had it, with only +# the ACME server pointed at the test's Pebble, as the current one is. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + namespace: 5stack + name: 5stack-issuer +spec: + acme: + server: https://pebble.pebble.svc:14000/dir + skipTLSVerify: true + email: hello@5stack.test + privateKeySecretRef: + name: 5stack-issuer + solvers: + - http01: + ingress: + ingressClassName: nginx From 8dae6e98d2265944fef4c99f47c408211fd7c08a Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 10:15:48 -0400 Subject: [PATCH 14/17] test: the nginx-era certificate is a 90-day one inside renewBefore, so it is due when cert-manager runs again, and the pin keeps the solver off port 80, since the router priority turns out not to be what lets it win Without the priority the solver Ingress still outranks the https-redirect route on port 80; that is now measured rather than pinned. --- tests/gateway/cert_manager_test.sh | 88 ++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/tests/gateway/cert_manager_test.sh b/tests/gateway/cert_manager_test.sh index 94f6624..79cd927 100644 --- a/tests/gateway/cert_manager_test.sh +++ b/tests/gateway/cert_manager_test.sh @@ -39,7 +39,7 @@ install_cert_manager() { for crd in certificates.cert-manager.io issuers.cert-manager.io clusterissuers.cert-manager.io; do kubectl wait --for=condition=Established "crd/$crd" --timeout=120s >/dev/null || return 1 done - wait_for_cert_manager >/dev/null + wait_for_cert_manager 300 >/dev/null } # The https overlay update.sh applies, rendered to $WORK_DIR/overlay.yaml, and @@ -77,17 +77,31 @@ PY } # What an ingress-nginx install leaves: the Issuer it had, the panel's -# Certificate, and the 5stack-ssl that Issuer gave it, here two days from -# expiring. cert-manager's controller is held at zero until the update has run: -# on a real install nothing is due during the update, and a renewal attempted -# here, with no ingress-nginx to serve its solver, would fail and back off for an -# hour. +# Certificate, and the 5stack-ssl that Issuer gave it -- a 90-day certificate +# with 5 days left, so inside renewBefore (240h) and due. cert-manager's +# controller is held at zero until the update has run: on a real install +# nothing is due during the update, and a renewal attempted here, with no +# ingress-nginx to serve its solver, would fail and back off for an hour. leave_an_nginx_era_certificate() { + local san="" host + for host in "$WEB" "$API" "$DEMOS" "$SEARCH" "$CONSOLE" "$WS" "$RELAY"; do + san="${san:+$san,}DNS:$host" + done kubectl -n cert-manager scale deployment cert-manager --replicas=0 >/dev/null || return 1 kubectl -n cert-manager wait --for=delete pod -l app.kubernetes.io/component=controller --timeout=120s >/dev/null 2>&1 kubectl apply -f "$HERE/fixtures/nginx-era-issuer.yaml" >/dev/null || return 1 cert_manager_object Certificate 5stack-ssl | kubectl apply -f - >/dev/null || return 1 - tls_secret 5stack-ssl "$WEB" "$API" "$DEMOS" "$SEARCH" "$CONSOLE" "$WS" "$RELAY" || return 1 + client openssl req -x509 -newkey rsa:2048 -nodes -subj "/CN=$WEB" -addext "subjectAltName=$san" \ + -not_before "$(date -u -d '-85 days' +%Y%m%d%H%M%SZ)" -not_after "$(date -u -d '+5 days' +%Y%m%d%H%M%SZ)" \ + -keyout /tmp/tls.key -out /tmp/tls.crt >/dev/null 2>&1 || return 1 + docker cp "$PREFIX-client:/tmp/tls.key" "$WORK_DIR/tls.key" >/dev/null + docker cp "$PREFIX-client:/tmp/tls.crt" "$WORK_DIR/tls.crt" >/dev/null + # Inside renewBefore, not just short-lived: cert-manager renews a + # certificate shorter than renewBefore at two thirds of its life instead. + openssl x509 -noout -checkend $((6 * 86400)) -in "$WORK_DIR/tls.crt" >/dev/null && return 1 + openssl x509 -noout -checkend $((4 * 86400)) -in "$WORK_DIR/tls.crt" >/dev/null || return 1 + kubectl -n 5stack create secret tls 5stack-ssl --cert="$WORK_DIR/tls.crt" --key="$WORK_DIR/tls.key" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null || return 1 kubectl -n 5stack annotate --overwrite secret 5stack-ssl cert-manager.io/issuer-name=5stack-issuer \ cert-manager.io/issuer-kind=Issuer cert-manager.io/issuer-group=cert-manager.io \ cert-manager.io/certificate-name=5stack-ssl >/dev/null || return 1 @@ -270,32 +284,33 @@ test_a_renewal_is_validated_and_served_again() { wait_until_served "$API" 5stack-ssl } -# The pin: the same Issuer without the solver's router priority. Its challenge -# is presented -- the solver Ingress exists -- but plain HTTP for the panel host -# keeps getting the https-redirect route's 308, so cert-manager never sees it -# reachable. The tests above pass only because the priority is there. -test_without_the_solver_priority_the_redirect_takes_the_challenge() { +# The pin: the same Issuer with its solver kept off port 80 (entrypoint +# websecure). Its challenge is presented -- the solver Ingress exists -- but +# plain HTTP for the panel host gets the https-redirect route's 308, which is +# what Let's Encrypt would see, and no certificate is issued. The same request +# shape is what the issuances above depended on reaching the solver. +test_with_the_solver_off_port_80_the_redirect_takes_the_challenge() { local token="" code cert_manager_object Issuer 5stack-issuer | python3 -I -c ' import sys d = sys.stdin.read() -assert "router.priority" in d -d = "\n".join(l for l in d.split("\n") if "router.priority" not in l) -print(d.replace("name: 5stack-issuer", "name: no-priority"))' | kubectl apply -f - >/dev/null || fail "could not make the Issuer" +assert "router.entrypoints: web\n" in d +d = d.replace("router.entrypoints: web\n", "router.entrypoints: websecure\n") +print(d.replace("name: 5stack-issuer", "name: off-port-80"))' | kubectl apply -f - >/dev/null || fail "could not make the Issuer" kubectl apply -f - >/dev/null </dev/null 2>&1 && fail "a certificate was issued without the priority" + kubectl -n 5stack get secret off-port-80 >/dev/null 2>&1 && fail "a certificate was issued with the solver off port 80" + kubectl -n 5stack delete certificate off-port-80 >/dev/null + kubectl -n 5stack delete issuer off-port-80 >/dev/null +} + +# Measured, not pinned: without the router priority the solver Ingress still +# outranks the https-redirect route on port 80 (Traefik's default priority for +# it is longer than the redirect's), so dropping it changes nothing today. +test_without_the_router_priority_the_solver_still_answers() { + cert_manager_object Issuer 5stack-issuer | python3 -I -c ' +import sys +d = sys.stdin.read() +assert "router.priority" in d +d = "\n".join(l for l in d.split("\n") if "router.priority" not in l) +print(d.replace("name: 5stack-issuer", "name: no-priority"))' | kubectl apply -f - >/dev/null || fail "could not make the Issuer" + kubectl apply -f - >/dev/null </dev/null || + fail "without the priority no certificate was issued" kubectl -n 5stack delete certificate no-priority >/dev/null kubectl -n 5stack delete issuer no-priority >/dev/null + kubectl -n 5stack delete secret no-priority >/dev/null } assert_pebble() { @@ -349,7 +392,8 @@ for name in \ test_the_panel_hosts_serve_it \ test_the_stream_certificate_is_issued_for_the_hls_host \ test_a_renewal_is_validated_and_served_again \ - test_without_the_solver_priority_the_redirect_takes_the_challenge; do + test_with_the_solver_off_port_80_the_redirect_takes_the_challenge \ + test_without_the_router_priority_the_solver_still_answers; do run_test "$name" done From 601757835939b7c40ad336ab858c6098a83e2cba Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 10:23:17 -0400 Subject: [PATCH 15/17] test: the cert-manager pin drops the solver's router priority for all the panel's hosts, since moving the solver off port 80 still issues once the ACME server follows the redirect to 443 Without the priority the solver Ingress is ranked by its rule's length, so the https-redirect route takes the challenge for some hosts and not others; the test records each host's answer and needs the redirect to win at least one and no certificate to be issued. --- tests/gateway/cert_manager_test.sh | 95 ++++++++++-------------------- 1 file changed, 31 insertions(+), 64 deletions(-) diff --git a/tests/gateway/cert_manager_test.sh b/tests/gateway/cert_manager_test.sh index 79cd927..6251639 100644 --- a/tests/gateway/cert_manager_test.sh +++ b/tests/gateway/cert_manager_test.sh @@ -284,77 +284,44 @@ test_a_renewal_is_validated_and_served_again() { wait_until_served "$API" 5stack-ssl } -# The pin: the same Issuer with its solver kept off port 80 (entrypoint -# websecure). Its challenge is presented -- the solver Ingress exists -- but -# plain HTTP for the panel host gets the https-redirect route's 308, which is -# what Let's Encrypt would see, and no certificate is issued. The same request -# shape is what the issuances above depended on reaching the solver. -test_with_the_solver_off_port_80_the_redirect_takes_the_challenge() { - local token="" code +# The pin: the panel's Issuer without the solver's router priority, for the +# panel's own hosts. Traefik then ranks the solver Ingress by its rule length, +# which for some of these hosts is below the https-redirect route: their +# challenge gets the 308, the redirect lands on the HTTPS listener where no +# solver is (it's on web only), and the certificate is never issued. Each +# host's answer is kept in $WORK_DIR/no-priority.codes for the job log. +test_without_the_solver_priority_the_redirect_takes_some_challenges() { + local challenges host token codes="" cert_manager_object Issuer 5stack-issuer | python3 -I -c ' import sys d = sys.stdin.read() -assert "router.entrypoints: web\n" in d -d = d.replace("router.entrypoints: web\n", "router.entrypoints: websecure\n") -print(d.replace("name: 5stack-issuer", "name: off-port-80"))' | kubectl apply -f - >/dev/null || fail "could not make the Issuer" - kubectl apply -f - >/dev/null </dev/null || fail "could not make the Issuer" + cert_manager_object Certificate 5stack-ssl | + sed -e 's/name: 5stack-ssl$/name: no-priority/' -e 's/secretName: 5stack-ssl$/secretName: no-priority/' \ + -e 's/name: 5stack-issuer$/name: no-priority/' | kubectl apply -f - >/dev/null || fail "could not make the Certificate" for _ in $(seq 1 60); do - token="$(kubectl -n 5stack get challenges \ - -o jsonpath='{.items[?(@.spec.issuerRef.name=="off-port-80")].spec.token}{"|"}{.items[?(@.spec.issuerRef.name=="off-port-80")].status.presented}')" - [ "${token#*|}" = true ] && break + challenges="$(kubectl -n 5stack get challenges -o jsonpath='{range .items[?(@.spec.issuerRef.name=="no-priority")]}{.spec.dnsName} {.spec.token} {.status.presented}{"\n"}{end}')" + [ "$(grep -c ' true$' <<<"$challenges")" = 7 ] && break sleep 2 done - [ "${token#*|}" = true ] || fail "the challenge was never presented" - token="${token%%|*}" - # Long past the few seconds Traefik takes to pick up the solver Ingress. + [ "$(grep -c ' true$' <<<"$challenges")" = 7 ] || fail "not every challenge was presented: $challenges" + # Long past the few seconds Traefik takes to pick up the solver Ingresses. sleep 15 - for _ in $(seq 1 5); do - call http "$API" "/.well-known/acme-challenge/$token" - code="$CODE" - [ "$code" = 308 ] || fail "with the solver off port 80 the challenge answered $code, not the redirect" - sleep 2 - done - kubectl -n 5stack get secret off-port-80 >/dev/null 2>&1 && fail "a certificate was issued with the solver off port 80" - kubectl -n 5stack delete certificate off-port-80 >/dev/null - kubectl -n 5stack delete issuer off-port-80 >/dev/null -} - -# Measured, not pinned: without the router priority the solver Ingress still -# outranks the https-redirect route on port 80 (Traefik's default priority for -# it is longer than the redirect's), so dropping it changes nothing today. -test_without_the_router_priority_the_solver_still_answers() { - cert_manager_object Issuer 5stack-issuer | python3 -I -c ' -import sys -d = sys.stdin.read() -assert "router.priority" in d -d = "\n".join(l for l in d.split("\n") if "router.priority" not in l) -print(d.replace("name: 5stack-issuer", "name: no-priority"))' | kubectl apply -f - >/dev/null || fail "could not make the Issuer" - kubectl apply -f - >/dev/null </dev/null || - fail "without the priority no certificate was issued" + while read -r host token _; do + call http "$host" "/.well-known/acme-challenge/$token" + codes="$codes $host=$CODE" + done <<<"$challenges" + echo "without the router priority:$codes" >"$WORK_DIR/no-priority.codes" + case "$codes" in + *=308*) ;; + *) fail "every host reached the solver without the priority:$codes" ;; + esac + sleep 30 + kubectl -n 5stack get secret no-priority >/dev/null 2>&1 && fail "a certificate was issued without the priority:$codes" kubectl -n 5stack delete certificate no-priority >/dev/null kubectl -n 5stack delete issuer no-priority >/dev/null - kubectl -n 5stack delete secret no-priority >/dev/null } assert_pebble() { @@ -392,9 +359,9 @@ for name in \ test_the_panel_hosts_serve_it \ test_the_stream_certificate_is_issued_for_the_hls_host \ test_a_renewal_is_validated_and_served_again \ - test_with_the_solver_off_port_80_the_redirect_takes_the_challenge \ - test_without_the_router_priority_the_solver_still_answers; do + test_without_the_solver_priority_the_redirect_takes_some_challenges; do run_test "$name" done +cat "$WORK_DIR/no-priority.codes" 2>/dev/null summary From 0768f57054f3427b923251c571b6a0927cd3be2d Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 10:23:54 -0400 Subject: [PATCH 16/17] feature: panel routes name their hosts, and web and the api compress their text responses Each panel HTTPRoute gets spec.hostnames equal to the hostname of the listener it attaches to, filled by the same replacements as the listeners in the https, reverse-proxy and dev overlays, so a route reads as its host rather than *. A route's hostnames intersect with its listener's, so routing is unchanged; a rendered-overlay check holds every route to its listener's host in all three. A compress Middleware, referenced by the web route and the api route's api rule only, compresses text/html, application/json, text/css, application/javascript and image/svg+xml responses of 1024 bytes or more. Demos, clips, uploads, playcast fragments, event streams, downloads and websockets pass through untouched, and so do responses that already carry a Content-Encoding and everything Hasura serves. --- base/api/httproute-relay.yaml | 2 + base/api/httproute-ws.yaml | 2 + base/api/httproute.yaml | 12 +++++ base/gateway/compress.yaml | 19 ++++++++ base/gateway/kustomization.yaml | 1 + base/hasura/httproute.yaml | 2 + base/rustfs/httproute.yaml | 2 + base/typesense/httproute.yaml | 2 + base/web/httproute.yaml | 10 +++- overlays/dev/hasura/httproute.yaml | 2 + overlays/dev/kustomization.yaml | 55 ++++++++++++++++++++++ overlays/dev/queues/httproute.yaml | 2 + overlays/http/http-replacements.yaml | 55 ++++++++++++++++++++++ overlays/http/https-replacements.yaml | 55 ++++++++++++++++++++++ overlays/mediamtx/httproute.yaml | 4 ++ tests/gateway/backend.py | 28 +++++++++-- tests/gateway/fresh_install_test.sh | 67 +++++++++++++++++++++++++++ tests/gateway/route_hostnames.py | 67 +++++++++++++++++++++++++++ 18 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 base/gateway/compress.yaml create mode 100644 tests/gateway/route_hostnames.py diff --git a/base/api/httproute-relay.yaml b/base/api/httproute-relay.yaml index 2d56dc9..4d9b6e6 100644 --- a/base/api/httproute-relay.yaml +++ b/base/api/httproute-relay.yaml @@ -7,6 +7,8 @@ spec: parentRefs: - name: 5stack sectionName: relay + hostnames: + - ${RELAY_DOMAIN} rules: - filters: - type: URLRewrite diff --git a/base/api/httproute-ws.yaml b/base/api/httproute-ws.yaml index 202bd3a..4022e0e 100644 --- a/base/api/httproute-ws.yaml +++ b/base/api/httproute-ws.yaml @@ -7,6 +7,8 @@ spec: parentRefs: - name: 5stack sectionName: ws + hostnames: + - ${WS_DOMAIN} rules: - filters: - type: URLRewrite diff --git a/base/api/httproute.yaml b/base/api/httproute.yaml index c6200d8..0dceed4 100644 --- a/base/api/httproute.yaml +++ b/base/api/httproute.yaml @@ -9,6 +9,8 @@ spec: parentRefs: - name: 5stack sectionName: api + hostnames: + - ${API_DOMAIN} rules: - matches: - path: @@ -92,6 +94,12 @@ spec: - path: type: PathPrefix value: /file-manager + filters: + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: compress backendRefs: - name: api port: 5585 @@ -118,6 +126,8 @@ spec: parentRefs: - name: 5stack sectionName: web + hostnames: + - ${WEB_DOMAIN} rules: - matches: - path: @@ -156,6 +166,8 @@ spec: parentRefs: - name: 5stack sectionName: demos + hostnames: + - ${DEMOS_DOMAIN} rules: - matches: - path: diff --git a/base/gateway/compress.yaml b/base/gateway/compress.yaml new file mode 100644 index 0000000..4911c9b --- /dev/null +++ b/base/gateway/compress.yaml @@ -0,0 +1,19 @@ +# Compresses text responses from web and the api (their routes reference it): +# only these types, so demos, clips, uploads, playcast fragments, event streams +# and downloads pass straight through, never compressed or held back. Responses +# under minResponseBodyBytes (1024) stay as they are, and Traefik never touches +# one that already has a Content-Encoding, so Nuxt's precompressed assets and +# Hasura's own gzip pass through too. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: compress + namespace: 5stack +spec: + compress: + includedContentTypes: + - text/html + - application/json + - text/css + - application/javascript + - image/svg+xml diff --git a/base/gateway/kustomization.yaml b/base/gateway/kustomization.yaml index cb3937d..d60a45d 100644 --- a/base/gateway/kustomization.yaml +++ b/base/gateway/kustomization.yaml @@ -4,3 +4,4 @@ kind: Kustomization resources: - gateway.yaml - ingressclass-nginx.yaml + - compress.yaml diff --git a/base/hasura/httproute.yaml b/base/hasura/httproute.yaml index 19549d6..d110e16 100644 --- a/base/hasura/httproute.yaml +++ b/base/hasura/httproute.yaml @@ -7,6 +7,8 @@ spec: parentRefs: - name: 5stack sectionName: api + hostnames: + - ${API_DOMAIN} rules: - matches: - path: diff --git a/base/rustfs/httproute.yaml b/base/rustfs/httproute.yaml index 24aed8c..1d66c32 100644 --- a/base/rustfs/httproute.yaml +++ b/base/rustfs/httproute.yaml @@ -7,6 +7,8 @@ spec: parentRefs: - name: 5stack sectionName: s3-console + hostnames: + - ${S3_CONSOLE_HOST} rules: - backendRefs: - name: rustfs diff --git a/base/typesense/httproute.yaml b/base/typesense/httproute.yaml index e8e893f..dfe6345 100644 --- a/base/typesense/httproute.yaml +++ b/base/typesense/httproute.yaml @@ -7,6 +7,8 @@ spec: parentRefs: - name: 5stack sectionName: typesense + hostnames: + - ${TYPESENSE_HOST} rules: - backendRefs: - name: typesense diff --git a/base/web/httproute.yaml b/base/web/httproute.yaml index f3ee210..8e6d31c 100644 --- a/base/web/httproute.yaml +++ b/base/web/httproute.yaml @@ -7,7 +7,15 @@ spec: parentRefs: - name: 5stack sectionName: web + hostnames: + - ${WEB_DOMAIN} rules: - - backendRefs: + - filters: + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: compress + backendRefs: - name: web port: 3000 diff --git a/overlays/dev/hasura/httproute.yaml b/overlays/dev/hasura/httproute.yaml index ea09825..f2c565e 100644 --- a/overlays/dev/hasura/httproute.yaml +++ b/overlays/dev/hasura/httproute.yaml @@ -7,6 +7,8 @@ spec: parentRefs: - name: 5stack sectionName: hasura-dashboard + hostnames: + - hasura.5stack.localhost rules: - backendRefs: - name: hasura diff --git a/overlays/dev/kustomization.yaml b/overlays/dev/kustomization.yaml index 0c07c7c..89a0373 100644 --- a/overlays/dev/kustomization.yaml +++ b/overlays/dev/kustomization.yaml @@ -16,6 +16,16 @@ replacements: name: api-config fieldPath: data.WEB_DOMAIN targets: + - select: + kind: HTTPRoute + name: api-web + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: web + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -36,6 +46,16 @@ replacements: name: api-config fieldPath: data.API_DOMAIN targets: + - select: + kind: HTTPRoute + name: api + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: hasura + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -56,6 +76,11 @@ replacements: name: api-config fieldPath: data.DEMOS_DOMAIN targets: + - select: + kind: HTTPRoute + name: demos + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -76,6 +101,11 @@ replacements: name: typesense-config fieldPath: data.TYPESENSE_HOST targets: + - select: + kind: HTTPRoute + name: typesense + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -96,6 +126,11 @@ replacements: name: s3-config fieldPath: data.S3_CONSOLE_HOST targets: + - select: + kind: HTTPRoute + name: s3-console + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -116,6 +151,11 @@ replacements: name: api-config fieldPath: data.WS_DOMAIN targets: + - select: + kind: HTTPRoute + name: ws + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -136,6 +176,11 @@ replacements: name: api-config fieldPath: data.RELAY_DOMAIN targets: + - select: + kind: HTTPRoute + name: relay + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -156,6 +201,16 @@ replacements: name: api-config fieldPath: data.GAME_STREAM_DOMAIN targets: + - select: + kind: HTTPRoute + name: mediamtx + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: snapshot + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack diff --git a/overlays/dev/queues/httproute.yaml b/overlays/dev/queues/httproute.yaml index 015f511..a4a0fcf 100644 --- a/overlays/dev/queues/httproute.yaml +++ b/overlays/dev/queues/httproute.yaml @@ -9,6 +9,8 @@ spec: parentRefs: - name: 5stack sectionName: queues + hostnames: + - queues.5stack.localhost rules: - matches: - path: diff --git a/overlays/http/http-replacements.yaml b/overlays/http/http-replacements.yaml index 2ef06e8..fb4dad1 100644 --- a/overlays/http/http-replacements.yaml +++ b/overlays/http/http-replacements.yaml @@ -4,6 +4,16 @@ replacements: name: api-config fieldPath: data.WEB_DOMAIN targets: + - select: + kind: HTTPRoute + name: api-web + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: web + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -27,6 +37,16 @@ replacements: name: api-config fieldPath: data.API_DOMAIN targets: + - select: + kind: HTTPRoute + name: api + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: hasura + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -42,6 +62,11 @@ replacements: name: api-config fieldPath: data.DEMOS_DOMAIN targets: + - select: + kind: HTTPRoute + name: demos + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -57,6 +82,11 @@ replacements: name: typesense-config fieldPath: data.TYPESENSE_HOST targets: + - select: + kind: HTTPRoute + name: typesense + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -72,6 +102,11 @@ replacements: name: s3-config fieldPath: data.S3_CONSOLE_HOST targets: + - select: + kind: HTTPRoute + name: s3-console + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -87,6 +122,11 @@ replacements: name: api-config fieldPath: data.WS_DOMAIN targets: + - select: + kind: HTTPRoute + name: ws + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -102,6 +142,11 @@ replacements: name: api-config fieldPath: data.RELAY_DOMAIN targets: + - select: + kind: HTTPRoute + name: relay + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -117,6 +162,16 @@ replacements: name: mediamtx-env fieldPath: data.GAME_STREAM_DOMAIN targets: + - select: + kind: HTTPRoute + name: mediamtx + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: snapshot + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack diff --git a/overlays/http/https-replacements.yaml b/overlays/http/https-replacements.yaml index 12b0aa4..53f65aa 100644 --- a/overlays/http/https-replacements.yaml +++ b/overlays/http/https-replacements.yaml @@ -4,6 +4,16 @@ replacements: name: api-config fieldPath: data.WEB_DOMAIN targets: + - select: + kind: HTTPRoute + name: api-web + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: web + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -32,6 +42,16 @@ replacements: name: api-config fieldPath: data.API_DOMAIN targets: + - select: + kind: HTTPRoute + name: api + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: hasura + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -52,6 +72,11 @@ replacements: name: api-config fieldPath: data.DEMOS_DOMAIN targets: + - select: + kind: HTTPRoute + name: demos + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -72,6 +97,11 @@ replacements: name: typesense-config fieldPath: data.TYPESENSE_HOST targets: + - select: + kind: HTTPRoute + name: typesense + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -92,6 +122,11 @@ replacements: name: s3-config fieldPath: data.S3_CONSOLE_HOST targets: + - select: + kind: HTTPRoute + name: s3-console + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -112,6 +147,11 @@ replacements: name: api-config fieldPath: data.WS_DOMAIN targets: + - select: + kind: HTTPRoute + name: ws + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -132,6 +172,11 @@ replacements: name: api-config fieldPath: data.RELAY_DOMAIN targets: + - select: + kind: HTTPRoute + name: relay + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack @@ -152,6 +197,16 @@ replacements: name: mediamtx-env fieldPath: data.GAME_STREAM_DOMAIN targets: + - select: + kind: HTTPRoute + name: mediamtx + fieldPaths: + - spec.hostnames.0 + - select: + kind: HTTPRoute + name: snapshot + fieldPaths: + - spec.hostnames.0 - select: kind: Gateway name: 5stack diff --git a/overlays/mediamtx/httproute.yaml b/overlays/mediamtx/httproute.yaml index f5778b9..f79b83f 100644 --- a/overlays/mediamtx/httproute.yaml +++ b/overlays/mediamtx/httproute.yaml @@ -13,6 +13,8 @@ spec: parentRefs: - name: 5stack sectionName: game-stream + hostnames: + - ${GAME_STREAM_DOMAIN} rules: # WHEP/WHIP, and the session URL trickle ICE PATCHes to. - matches: @@ -64,6 +66,8 @@ spec: parentRefs: - name: 5stack sectionName: game-stream + hostnames: + - ${GAME_STREAM_DOMAIN} rules: - matches: - path: diff --git a/tests/gateway/backend.py b/tests/gateway/backend.py index d0d0c2e..b7103ca 100644 --- a/tests/gateway/backend.py +++ b/tests/gateway/backend.py @@ -4,6 +4,10 @@ Service named like the real one reaches it on the real port. Every response is JSON describing the request as it arrived, which is what the tests assert on. +/payload?type=T&bytes=N answers N compressible bytes as Content-Type T, gzipped +by the backend itself with &encoded=gzip; /stream?seconds=S&type=T sends a line +a second, flushed, for S seconds. + /streams/authorize, /plugins/authorize and /qx/authorize answer like the api's forward-auth on branch feature/forward-auth-traefik: X-Original-* and X-Forwarded-* that disagree are refused, a forwarded OPTIONS is a preflight, @@ -11,6 +15,7 @@ """ import base64 +import gzip import hashlib import json import os @@ -152,9 +157,21 @@ def websocket(self): pass self.close_connection = True - def stream(self, seconds): + def payload(self, query): + size = int(query.get("bytes", ["20000"])[0]) + data = (b"5stack " * (size // 7 + 1))[:size] self.send_response(200) - self.send_header("Content-Type", "text/plain") + self.send_header("Content-Type", query.get("type", ["application/json"])[0]) + if query.get("encoded", [""])[0] == "gzip": + data = gzip.compress(data) + self.send_header("Content-Encoding", "gzip") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def stream(self, seconds, content_type="text/plain"): + self.send_response(200) + self.send_header("Content-Type", content_type) self.send_header("Transfer-Encoding", "chunked") self.end_headers() for i in range(seconds): @@ -180,8 +197,11 @@ def handle_any(self): AUTH_LOG.clear() return self.reply(200, log) if path.endswith("/stream"): - seconds = int(parse_qs(urlsplit(self.path).query).get("seconds", ["5"])[0]) - return self.stream(seconds) + query = parse_qs(urlsplit(self.path).query) + return self.stream(int(query.get("seconds", ["5"])[0]), query.get("type", ["text/plain"])[0]) + if path.endswith("/payload"): + self.read_body() + return self.payload(parse_qs(urlsplit(self.path).query)) body_bytes = self.read_body() if self.command == "POST" and path.endswith("/whep"): return self.reply(201, self.describe(body_bytes), {"Location": path + "/session-1"}) diff --git a/tests/gateway/fresh_install_test.sh b/tests/gateway/fresh_install_test.sh index e914fba..7d55ad5 100644 --- a/tests/gateway/fresh_install_test.sh +++ b/tests/gateway/fresh_install_test.sh @@ -407,6 +407,69 @@ test_an_upgrade_renews_a_shared_certificate_about_to_expire() { wait_for 200 https both.test /x } +# Every panel route names its listener's host, so it reads as that host rather +# than "*" -- in each overlay update.sh or dev.sh applies. +test_every_panel_route_names_its_listeners_host() { + local overlay file + write_overlays || fail "write_overlays failed" + for file in "$PANEL_DIR"/overlays/local-secrets/*.env; do + [ -f "$PANEL_DIR/overlays/dev/secrets/${file##*/}" ] || cp "$file" "$PANEL_DIR/overlays/dev/secrets/" + done + for overlay in local-secrets-https local-secrets-http dev; do + "$PANEL_DIR/kustomize" build "$PANEL_DIR/overlays/$overlay" >"$WORK_DIR/$overlay.yaml" || fail "$overlay did not build" + python3 -I "$HERE/route_hostnames.py" "$WORK_DIR/$overlay.yaml" >"$WORK_DIR/hostnames.out" || + fail "$overlay: $(cat "$WORK_DIR/hostnames.out")" + done +} + +# GET https://$1$2 asking for gzip: the response's Content-Encoding ("none" +# without one) and the size of its body once decoded. +fetch_asking_for_gzip() { + # shellcheck disable=SC2016 # expanded by the client container's sh + client sh -c 'curl -sk --resolve "$1:443:$2" -H "Accept-Encoding: gzip" -D /tmp/h -o /tmp/b "https://$1$3" + encoding="$(tr -d "\r" = 3.5, f"first byte after {first}s of {total}s" +assert encoding == "none", f"compressed: {encoding}"' "$out" || fail "$out" +} + start_long_requests() { client sh -c 'head -c 125829120 /dev/zero >/tmp/big' docker exec -d "$PREFIX-client" sh -c "curl -sk --resolve $DEMOS:443:$NODE_IP -X PUT --data-binary @/tmp/big \ @@ -501,6 +564,7 @@ test_http_mode_also_answers_on_443_for_a_proxy_that_forwards_there() { start_cluster || exit 1 make_panel || exit 1 +run_test test_every_panel_route_names_its_listeners_host install_traefik "$PANEL_DIR/overlays/config/traefik-config.env" || exit 1 deploy_panel_stubs || exit 1 routing_layer https || exit 1 @@ -529,6 +593,9 @@ for name in \ test_ws_and_relay_hosts_rewrite_into_the_api \ test_websocket_upgrades_through_the_ws_host \ test_typesense_and_s3_console_hosts \ + test_text_from_web_and_the_api_is_compressed \ + test_binary_and_already_encoded_responses_pass_through \ + test_event_streams_are_neither_compressed_nor_held_back \ test_mediamtx_preflight_is_answered_before_forward_auth \ test_mediamtx_requires_a_session \ test_whep_post_and_patch_pass_forward_auth \ diff --git a/tests/gateway/route_hostnames.py b/tests/gateway/route_hostnames.py new file mode 100644 index 0000000..88b975a --- /dev/null +++ b/tests/gateway/route_hostnames.py @@ -0,0 +1,67 @@ +"""Every HTTPRoute in a rendered overlay names the hosts of the listeners it +attaches to. A route's hostnames intersect with its listener's, so equal ones +change nothing about routing, but put the host on the route itself. + + python3 route_hostnames.py overlay.yaml +""" + +import re +import sys + + +def block(doc, key, indent): + """The lines of `key:` at `indent` spaces, up to the next key at that indent.""" + lines, inside = [], False + for line in doc.split("\n"): + if line == " " * indent + key + ":": + inside = True + elif inside and (not line.startswith(" " * indent) or re.match(rf"^ {{{indent}}}\S", line) and not line.startswith(" " * indent + "- ")): + break + elif inside: + lines.append(line) + return lines + + +def items(lines, indent): + """Each `- ` item of a list at `indent`, as a dict of its scalar fields.""" + out, current = [], None + for line in lines: + m = re.match(rf"^ {{{indent}}}- (\S+): (.*)$", line) or re.match(rf"^ {{{indent}}} (\S+): (.*)$", line) + if line.startswith(" " * indent + "- "): + current = {} + out.append(current) + if m and current is not None: + current[m[1]] = m[2].strip('"') + return out + + +docs = open(sys.argv[1]).read().split("\n---\n") +kind = lambda d: (re.search(r"(?m)^kind: (\S+)$", d) or [None, None])[1] +meta = lambda d, f: (re.search(rf"(?m)^ {f}: (\S+)$", d.split("\nspec:", 1)[0]) or [None, None])[1] + +listeners = {} +for d in docs: + if kind(d) == "Gateway": + for listener in items(block(d.split("\nstatus:", 1)[0], "listeners", 2), 2): + listeners[(meta(d, "namespace"), meta(d, "name"), listener["name"])] = listener.get("hostname") + +problems, attachments = [], 0 +for d in docs: + if kind(d) != "HTTPRoute": + continue + namespace, name = meta(d, "namespace"), meta(d, "name") + hostnames = [re.sub(r"^ *- ", "", l).strip('"') for l in block(d, "hostnames", 2) if l.strip().startswith("- ")] + if any("${" in h for h in hostnames): + problems.append(f"{namespace}/{name}: unreplaced hostnames {hostnames}") + for ref in items(block(d, "parentRefs", 2), 2): + key = (ref.get("namespace", namespace), ref["name"], ref.get("sectionName")) + if key not in listeners or listeners[key] is None: + continue + attachments += 1 + if hostnames != [listeners[key]]: + problems.append(f"{namespace}/{name} -> {key[1]}/{key[2]} ({listeners[key]}): hostnames {hostnames}") + +if problems or not attachments: + print("\n".join(problems) or "no route is attached to a listener with a hostname") + sys.exit(1) +print(f"{attachments} route attachments name their listener's host") From e12b2cd4d3f824f44806fdb44bde8ec824ad959d Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 29 Oct 2026 10:29:05 -0400 Subject: [PATCH 17/17] test: the cert-manager pin takes the solver off both 80 and 443, the one break that stops issuance through Traefik Two runs without the router priority still issued: once the solver pod is up its Ingress outranks the https-redirect route, and a solver on websecure is reached through the redirect, so neither annotation alone decides it on Traefik v3.7.16. With the solver on neither port, the redirect answers the challenge, the ACME server follows it to 443 where there's no solver, and the Certificate never becomes Ready. --- tests/gateway/cert_manager_test.sh | 76 +++++++++++++++++------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/tests/gateway/cert_manager_test.sh b/tests/gateway/cert_manager_test.sh index 6251639..efd708e 100644 --- a/tests/gateway/cert_manager_test.sh +++ b/tests/gateway/cert_manager_test.sh @@ -284,44 +284,55 @@ test_a_renewal_is_validated_and_served_again() { wait_until_served "$API" 5stack-ssl } -# The pin: the panel's Issuer without the solver's router priority, for the -# panel's own hosts. Traefik then ranks the solver Ingress by its rule length, -# which for some of these hosts is below the https-redirect route: their -# challenge gets the 308, the redirect lands on the HTTPS listener where no -# solver is (it's on web only), and the certificate is never issued. Each -# host's answer is kept in $WORK_DIR/no-priority.codes for the job log. -test_without_the_solver_priority_the_redirect_takes_some_challenges() { - local challenges host token codes="" +# The pin: the panel's Issuer with its solver on neither port 80 nor 443 +# (Traefik's metrics entrypoint). The https-redirect route then answers the +# challenge on port 80, the ACME server follows it to 443 where there's no +# solver either, and nothing is issued: the issuances above stand on the solver +# winning that path. Neither of its annotations alone decides that on Traefik +# v3.7.16 -- the solver Ingress outranks the redirect without the priority, and +# a solver on websecure is reached through the redirect -- so the pin takes +# both ports away. +test_with_no_solver_on_80_or_443_the_redirect_takes_the_challenge() { + local token="" ready cert_manager_object Issuer 5stack-issuer | python3 -I -c ' import sys d = sys.stdin.read() -assert "router.priority" in d -d = "\n".join(l for l in d.split("\n") if "router.priority" not in l) -print(d.replace("name: 5stack-issuer", "name: no-priority"))' | kubectl apply -f - >/dev/null || fail "could not make the Issuer" - cert_manager_object Certificate 5stack-ssl | - sed -e 's/name: 5stack-ssl$/name: no-priority/' -e 's/secretName: 5stack-ssl$/secretName: no-priority/' \ - -e 's/name: 5stack-issuer$/name: no-priority/' | kubectl apply -f - >/dev/null || fail "could not make the Certificate" +assert "router.entrypoints: web\n" in d +d = d.replace("router.entrypoints: web\n", "router.entrypoints: metrics\n") +print(d.replace("name: 5stack-issuer", "name: no-solver"))' | kubectl apply -f - >/dev/null || fail "could not make the Issuer" + kubectl apply -f - >/dev/null <"$WORK_DIR/no-priority.codes" - case "$codes" in - *=308*) ;; - *) fail "every host reached the solver without the priority:$codes" ;; - esac sleep 30 - kubectl -n 5stack get secret no-priority >/dev/null 2>&1 && fail "a certificate was issued without the priority:$codes" - kubectl -n 5stack delete certificate no-priority >/dev/null - kubectl -n 5stack delete issuer no-priority >/dev/null + ready="$(kubectl -n 5stack get certificate no-solver -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" + [ "$ready" != True ] || fail "a certificate was issued with no solver on 80 or 443" + kubectl -n 5stack delete certificate no-solver >/dev/null + kubectl -n 5stack delete issuer no-solver >/dev/null } assert_pebble() { @@ -359,9 +370,8 @@ for name in \ test_the_panel_hosts_serve_it \ test_the_stream_certificate_is_issued_for_the_hls_host \ test_a_renewal_is_validated_and_served_again \ - test_without_the_solver_priority_the_redirect_takes_some_challenges; do + test_with_no_solver_on_80_or_443_the_redirect_takes_the_challenge; do run_test "$name" done -cat "$WORK_DIR/no-priority.codes" 2>/dev/null summary