From fcf50071b220c97b54649c170164a08b8bbe68c9 Mon Sep 17 00:00:00 2001 From: Steve Kriss Date: Tue, 7 May 2024 14:04:25 -0600 Subject: [PATCH] v1.29.0 docs and config updates (#6424) * Prepare documentation site for v1.29.0 release. * Add changelog for v1.29.0 release. * update compat matrix, versions.yaml * update dependabot, trivy configs Signed-off-by: Steve Kriss --- .github/dependabot.yml | 14 +- .github/workflows/trivy-scan.yaml | 2 +- changelogs/CHANGELOG-v1.29.0.md | 144 + .../unreleased/6145-skriss-deprecation.md | 4 - .../unreleased/6146-skriss-deprecation.md | 5 - changelogs/unreleased/6146-skriss-major.md | 25 - .../unreleased/6149-izturn-deprecation.md | 3 - changelogs/unreleased/6149-izturn-minor.md | 5 - .../unreleased/6153-sunjayBhatia-major.md | 5 - .../unreleased/6185-sunjayBhatia-small.md | 1 - .../unreleased/6188-lubronzhan-minor.md | 10 - changelogs/unreleased/6230-yangyy93-small.md | 1 - changelogs/unreleased/6246-skriss-small.md | 1 - changelogs/unreleased/6250-skriss-small.md | 1 - changelogs/unreleased/6271-SamMHD-minor.md | 4 - changelogs/unreleased/6295-tsaarni-small.md | 1 - changelogs/unreleased/6297-rajatvig-small.md | 1 - changelogs/unreleased/6327-skriss-small.md | 1 - changelogs/unreleased/6335-skriss-small.md | 1 - changelogs/unreleased/6353-tico88612-small.md | 1 - changelogs/unreleased/6375-skriss-small.md | 1 - .../unreleased/6413-sunjayBhatia-small.md | 1 - site/config.yaml | 3 +- site/content/docs/1.29/_index.md | 48 + site/content/docs/1.29/architecture.md | 74 + .../docs/1.29/config/access-logging.md | 148 + site/content/docs/1.29/config/annotations.md | 98 + .../docs/1.29/config/api-reference.html | 9109 +++++++++++++++++ site/content/docs/1.29/config/api.md | 3 + .../docs/1.29/config/client-authorization.md | 123 + .../docs/1.29/config/cookie-rewriting.md | 109 + site/content/docs/1.29/config/cors.md | 82 + .../1.29/config/external-service-routing.md | 47 + site/content/docs/1.29/config/fundamentals.md | 197 + site/content/docs/1.29/config/gateway-api.md | 217 + .../content/docs/1.29/config/health-checks.md | 160 + .../docs/1.29/config/inclusion-delegation.md | 139 + site/content/docs/1.29/config/ingress.md | 94 + site/content/docs/1.29/config/ip-filtering.md | 80 + .../docs/1.29/config/jwt-verification.md | 182 + .../docs/1.29/config/overload-manager.md | 30 + .../content/docs/1.29/config/rate-limiting.md | 366 + .../docs/1.29/config/request-rewriting.md | 337 + .../docs/1.29/config/request-routing.md | 535 + site/content/docs/1.29/config/slow-start.md | 39 + .../docs/1.29/config/tls-delegation.md | 79 + .../docs/1.29/config/tls-termination.md | 353 + site/content/docs/1.29/config/tracing.md | 117 + site/content/docs/1.29/config/upstream-tls.md | 108 + .../content/docs/1.29/config/virtual-hosts.md | 138 + site/content/docs/1.29/config/websockets.md | 27 + site/content/docs/1.29/configuration.md | 541 + site/content/docs/1.29/deploy-options.md | 383 + site/content/docs/1.29/github.md | 80 + site/content/docs/1.29/grpc-tls-howto.md | 169 + site/content/docs/1.29/guides/_index.md | 9 + site/content/docs/1.29/guides/cert-manager.md | 670 ++ .../docs/1.29/guides/deploy-aws-nlb.md | 47 + .../docs/1.29/guides/deploy-aws-tls-nlb.md | 135 + .../1.29/guides/external-authorization.md | 538 + site/content/docs/1.29/guides/fips.md | 169 + site/content/docs/1.29/guides/gatekeeper.md | 456 + site/content/docs/1.29/guides/gateway-api.md | 212 + .../docs/1.29/guides/global-rate-limiting.md | 503 + site/content/docs/1.29/guides/grpc.md | 225 + .../docs/1.29/guides/health-checking.md | 11 + site/content/docs/1.29/guides/kind.md | 63 + .../content/docs/1.29/guides/metrics/table.md | 20 + site/content/docs/1.29/guides/prometheus.md | 94 + site/content/docs/1.29/guides/proxy-proto.md | 53 + .../docs/1.29/guides/resource-limits.md | 161 + site/content/docs/1.29/img/archoverview.png | Bin 0 -> 78807 bytes .../1.29/img/contour_deployment_in_k8s.png | Bin 0 -> 134492 bytes .../content/docs/1.29/img/shutdownmanager.png | Bin 0 -> 51051 bytes .../1.29/img/source/shutdownmanager.drawio | 1 + site/content/docs/1.29/redeploy-envoy.md | 72 + site/content/docs/1.29/start-contributing.md | 130 + site/content/docs/1.29/troubleshooting.md | 41 + .../troubleshooting/common-proxy-errors.md | 96 + .../1.29/troubleshooting/contour-debug-log.md | 6 + .../1.29/troubleshooting/contour-graph.md | 25 + .../troubleshooting/contour-xds-resources.md | 19 + .../troubleshooting/envoy-admin-interface.md | 32 + .../envoy-container-draining.md | 29 + .../1.29/troubleshooting/envoy-debug-log.md | 8 + .../1.29/troubleshooting/profiling-contour.md | 14 + .../content/resources/compatibility-matrix.md | 1 + site/data/docs/1-29-toc.yml | 151 + site/data/docs/toc-mapping.yml | 1 + versions.yaml | 12 +- 90 files changed, 18369 insertions(+), 82 deletions(-) create mode 100644 changelogs/CHANGELOG-v1.29.0.md delete mode 100644 changelogs/unreleased/6145-skriss-deprecation.md delete mode 100644 changelogs/unreleased/6146-skriss-deprecation.md delete mode 100644 changelogs/unreleased/6146-skriss-major.md delete mode 100644 changelogs/unreleased/6149-izturn-deprecation.md delete mode 100644 changelogs/unreleased/6149-izturn-minor.md delete mode 100644 changelogs/unreleased/6153-sunjayBhatia-major.md delete mode 100644 changelogs/unreleased/6185-sunjayBhatia-small.md delete mode 100644 changelogs/unreleased/6188-lubronzhan-minor.md delete mode 100644 changelogs/unreleased/6230-yangyy93-small.md delete mode 100644 changelogs/unreleased/6246-skriss-small.md delete mode 100644 changelogs/unreleased/6250-skriss-small.md delete mode 100644 changelogs/unreleased/6271-SamMHD-minor.md delete mode 100644 changelogs/unreleased/6295-tsaarni-small.md delete mode 100644 changelogs/unreleased/6297-rajatvig-small.md delete mode 100644 changelogs/unreleased/6327-skriss-small.md delete mode 100644 changelogs/unreleased/6335-skriss-small.md delete mode 100644 changelogs/unreleased/6353-tico88612-small.md delete mode 100644 changelogs/unreleased/6375-skriss-small.md delete mode 100644 changelogs/unreleased/6413-sunjayBhatia-small.md create mode 100644 site/content/docs/1.29/_index.md create mode 100644 site/content/docs/1.29/architecture.md create mode 100644 site/content/docs/1.29/config/access-logging.md create mode 100644 site/content/docs/1.29/config/annotations.md create mode 100644 site/content/docs/1.29/config/api-reference.html create mode 100644 site/content/docs/1.29/config/api.md create mode 100644 site/content/docs/1.29/config/client-authorization.md create mode 100644 site/content/docs/1.29/config/cookie-rewriting.md create mode 100644 site/content/docs/1.29/config/cors.md create mode 100644 site/content/docs/1.29/config/external-service-routing.md create mode 100644 site/content/docs/1.29/config/fundamentals.md create mode 100644 site/content/docs/1.29/config/gateway-api.md create mode 100644 site/content/docs/1.29/config/health-checks.md create mode 100644 site/content/docs/1.29/config/inclusion-delegation.md create mode 100644 site/content/docs/1.29/config/ingress.md create mode 100644 site/content/docs/1.29/config/ip-filtering.md create mode 100644 site/content/docs/1.29/config/jwt-verification.md create mode 100644 site/content/docs/1.29/config/overload-manager.md create mode 100644 site/content/docs/1.29/config/rate-limiting.md create mode 100644 site/content/docs/1.29/config/request-rewriting.md create mode 100644 site/content/docs/1.29/config/request-routing.md create mode 100644 site/content/docs/1.29/config/slow-start.md create mode 100644 site/content/docs/1.29/config/tls-delegation.md create mode 100644 site/content/docs/1.29/config/tls-termination.md create mode 100644 site/content/docs/1.29/config/tracing.md create mode 100644 site/content/docs/1.29/config/upstream-tls.md create mode 100644 site/content/docs/1.29/config/virtual-hosts.md create mode 100644 site/content/docs/1.29/config/websockets.md create mode 100644 site/content/docs/1.29/configuration.md create mode 100644 site/content/docs/1.29/deploy-options.md create mode 100644 site/content/docs/1.29/github.md create mode 100644 site/content/docs/1.29/grpc-tls-howto.md create mode 100644 site/content/docs/1.29/guides/_index.md create mode 100644 site/content/docs/1.29/guides/cert-manager.md create mode 100644 site/content/docs/1.29/guides/deploy-aws-nlb.md create mode 100644 site/content/docs/1.29/guides/deploy-aws-tls-nlb.md create mode 100644 site/content/docs/1.29/guides/external-authorization.md create mode 100644 site/content/docs/1.29/guides/fips.md create mode 100644 site/content/docs/1.29/guides/gatekeeper.md create mode 100644 site/content/docs/1.29/guides/gateway-api.md create mode 100644 site/content/docs/1.29/guides/global-rate-limiting.md create mode 100644 site/content/docs/1.29/guides/grpc.md create mode 100644 site/content/docs/1.29/guides/health-checking.md create mode 100644 site/content/docs/1.29/guides/kind.md create mode 100644 site/content/docs/1.29/guides/metrics/table.md create mode 100644 site/content/docs/1.29/guides/prometheus.md create mode 100644 site/content/docs/1.29/guides/proxy-proto.md create mode 100644 site/content/docs/1.29/guides/resource-limits.md create mode 100644 site/content/docs/1.29/img/archoverview.png create mode 100644 site/content/docs/1.29/img/contour_deployment_in_k8s.png create mode 100644 site/content/docs/1.29/img/shutdownmanager.png create mode 100644 site/content/docs/1.29/img/source/shutdownmanager.drawio create mode 100644 site/content/docs/1.29/redeploy-envoy.md create mode 100644 site/content/docs/1.29/start-contributing.md create mode 100644 site/content/docs/1.29/troubleshooting.md create mode 100644 site/content/docs/1.29/troubleshooting/common-proxy-errors.md create mode 100644 site/content/docs/1.29/troubleshooting/contour-debug-log.md create mode 100644 site/content/docs/1.29/troubleshooting/contour-graph.md create mode 100644 site/content/docs/1.29/troubleshooting/contour-xds-resources.md create mode 100644 site/content/docs/1.29/troubleshooting/envoy-admin-interface.md create mode 100644 site/content/docs/1.29/troubleshooting/envoy-container-draining.md create mode 100644 site/content/docs/1.29/troubleshooting/envoy-debug-log.md create mode 100644 site/content/docs/1.29/troubleshooting/profiling-contour.md create mode 100644 site/data/docs/1-29-toc.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0886364fb0b..0d64f74c423 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -35,9 +35,9 @@ updates: patterns: - "actions/upload-artifact" - "actions/download-artifact" - + # release branch N targets -- target-branch: release-1.28 +- target-branch: release-1.29 package-ecosystem: "gomod" directory: "/" schedule: @@ -57,7 +57,7 @@ updates: k8s-dependencies: patterns: - "k8s.io/*" -- target-branch: release-1.28 +- target-branch: release-1.29 package-ecosystem: "github-actions" directory: "/" schedule: @@ -80,7 +80,7 @@ updates: - "actions/download-artifact" # release branch N-1 targets -- target-branch: release-1.27 +- target-branch: release-1.28 package-ecosystem: "gomod" directory: "/" schedule: @@ -100,7 +100,7 @@ updates: k8s-dependencies: patterns: - "k8s.io/*" -- target-branch: release-1.27 +- target-branch: release-1.28 package-ecosystem: "github-actions" directory: "/" schedule: @@ -123,7 +123,7 @@ updates: - "actions/download-artifact" # release branch N-2 targets -- target-branch: release-1.26 +- target-branch: release-1.27 package-ecosystem: "gomod" directory: "/" schedule: @@ -143,7 +143,7 @@ updates: k8s-dependencies: patterns: - "k8s.io/*" -- target-branch: release-1.26 +- target-branch: release-1.27 package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/trivy-scan.yaml b/.github/workflows/trivy-scan.yaml index 78136a3cad5..0cfb6170762 100644 --- a/.github/workflows/trivy-scan.yaml +++ b/.github/workflows/trivy-scan.yaml @@ -16,9 +16,9 @@ jobs: matrix: branch: - main + - release-1.29 - release-1.28 - release-1.27 - - release-1.26 runs-on: ubuntu-latest permissions: security-events: write diff --git a/changelogs/CHANGELOG-v1.29.0.md b/changelogs/CHANGELOG-v1.29.0.md new file mode 100644 index 00000000000..09e9394a5b2 --- /dev/null +++ b/changelogs/CHANGELOG-v1.29.0.md @@ -0,0 +1,144 @@ +We are delighted to present version v1.29.0 of Contour, our layer 7 HTTP reverse proxy for Kubernetes clusters. + +A big thank you to everyone who contributed to the release. + + +- [Major Changes](#major-changes) +- [Minor Changes](#minor-changes) +- [Other Changes](#other-changes) +- [Docs Changes](#docs-changes) +- [Deprecations/Removals](#deprecation-and-removal-notices) +- [Installing/Upgrading](#installing-and-upgrading) +- [Compatible Kubernetes Versions](#compatible-kubernetes-versions) +- [Community Thanks!](#community-thanks) + +# Major Changes + +## Default xDS Server Implementation is now Envoy + +As of this release, Contour now uses the `envoy` xDS server implementation by default. +This xDS server implementation is based on Envoy's [go-control-plane project](https://github.com/envoyproxy/go-control-plane) and will eventually be the only supported xDS server implementation in Contour. +This change is expected to be transparent to users. + +### I'm seeing issues after upgrading, how to I revert to the contour xDS server? + +If you encounter any issues, you can easily revert to the `contour` xDS server with the following configuration: + +(if using Contour config file) +```yaml +server: + xds-server-type: contour +``` + +(if using ContourConfiguration CRD) +```yaml +... +spec: + xdsServer: + type: contour +``` + +You will need to restart Contour for the changes to take effect. + +(#6146, @skriss) + +## Gateway API: Inform on v1 types + +Contour no longer informs on v1beta1 resources that have graduated to v1. +This includes the "core" resources GatewayClass, Gateway, and HTTPRoute. +This means that users should ensure they have updated CRDs to Gateway API v1.0.0 or newer, which introduced the v1 version with compatibility with v1beta1. + +(#6153, @sunjayBhatia) + + +# Minor Changes + +## Use EndpointSlices by default + +Contour now uses the Kubernetes EndpointSlices API by default to determine the endpoints to configure Envoy, instead of the Endpoints API. +Note: if you need to continue using the Endpoints API, you can disable the feature flag via `featureFlags: ["useEndpointSlices=false"]` in the Contour config file or ContourConfiguration CRD. + +(#6149, @izturn) + +## Gateway API: handle Route conflicts with HTTPRoute.Matches + +It's possible that multiple HTTPRoutes will define the same Match conditions. In this case the following logic is applied to resolve the conflict: + +- 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”. +- The Route appearing first in alphabetical order (namespace/name) for example, foo/bar is given precedence over foo/baz. + +With above ordering, any HTTPRoute that ranks lower, will be marked with below conditions accordionly +1. If only partial rules under this HTTPRoute are conflicted, it's marked with `Accepted: True` and `PartiallyInvalid: true` Conditions and Reason: `RuleMatchPartiallyConflict`. +2. If all the rules under this HTTPRoute are conflicted, it's marked with `Accepted: False` Condition and Reason `RuleMatchConflict`. + +(#6188, @lubronzhan) + +## Spawn Upstream Span is now enabled in tracing + +As described in [Envoy documentations](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-tracing), ```spawn_upstream_span``` should be true when envoy is working as an independent proxy and from now on contour tracing spans will show up as a parent span to upstream spans. + +(#6271, @SamMHD) + + +# Other Changes +- Fix data race in BackendTLSPolicy status update logic. (#6185, @sunjayBhatia) +- Fix for specifying a health check port with an ExternalName Service. (#6230, @yangyy93) +- Updates the example `envoyproxy/ratelimit` image tag to `19f2079f`, for multi-arch support and other improvements. (#6246, @skriss) +- In the `envoy` go-control-plane xDS server, use a separate snapshot cache for Endpoints, to minimize the amount of unnecessary xDS traffic generated. (#6250, @skriss) +- If there were no relevant resources for Contour in the watched namespaces during the startup of a follower instance of Contour, it did not reach a ready state. (#6295, @tsaarni) +- Added support for enabling circuit breaker statistics tracking. (#6297, @rajatvig) +- Updates to Go 1.22.2. See the [Go release notes](https://go.dev/doc/devel/release#go1.22.minor) for more information. (#6327, @skriss) +- Gateway API: add support for HTTPRoute's Timeouts.BackendRequest field. (#6335, @skriss) +- Updates Envoy to v1.30.1. See the v1.30.0 release notes [here](https://www.envoyproxy.io/docs/envoy/v1.30.1/version_history/v1.30/v1.30.0) and the v1.30.1 release notes [here](https://www.envoyproxy.io/docs/envoy/v1.30.1/version_history/v1.30/v1.30.1). (#6353, @tico88612) +- Gateway API: a timeout value of `0s` disables the timeout. (#6375, @skriss) +- Fix provisioner to use separate `--disable-feature` flags on Contour Deployment for each disabled feature. Previously a comma separated list was passed which was incorrect. (#6413, @sunjayBhatia) + + +# Deprecation and Removal Notices + +## Configuring Contour with a GatewayClass controller name is no longer supported + +Contour can no longer be configured with a GatewayClass controller name (gateway.controllerName in the config file or ContourConfiguration CRD), as the config field has been removed. +Instead, either use a specific Gateway reference (gateway.gatewayRef), or use the Gateway provisioner. + +(#6145, @skriss) + +## Contour xDS server implementation is now deprecated + +As of this release, the `contour` xDS server implementation is now deprecated. +Once the go-control-plane based `envoy` xDS server has had sufficient production bake time, the `contour` implementation will be removed from Contour. +Notification of removal will occur at least one release in advance. + +(#6146, @skriss) + +## Use of Endpoints API is deprecated + +Contour now uses the EndpointSlices API by default, and its usage of the Endpoints API is deprecated as of this release. Support for Endpoints, and the associated `useEndpointSlices` feature flag, will be removed in a future release. + +(#6149, @izturn) + + +# Installing and Upgrading + +For a fresh install of Contour, consult the [getting started documentation](https://projectcontour.io/getting-started/). + +To upgrade an existing Contour installation, please consult the [upgrade documentation](https://projectcontour.io/resources/upgrading/). + + +# Compatible Kubernetes Versions + +Contour v1.29.0 is tested against Kubernetes 1.27 through 1.29. + +# Community Thanks! +We’re immensely grateful for all the community contributions that help make Contour even better! For this release, special thanks go out to the following contributors: + +- @SamMHD +- @izturn +- @lubronzhan +- @rajatvig +- @tico88612 +- @yangyy93 + + +# Are you a Contour user? We would love to know! +If you're using Contour and want to add your organization to our adopters list, please visit this [page](https://projectcontour.io/resources/adopters/). If you prefer to keep your organization name anonymous but still give us feedback into your usage and scenarios for Contour, please post on this [GitHub thread](https://github.com/projectcontour/contour/issues/1269). diff --git a/changelogs/unreleased/6145-skriss-deprecation.md b/changelogs/unreleased/6145-skriss-deprecation.md deleted file mode 100644 index 168e7666284..00000000000 --- a/changelogs/unreleased/6145-skriss-deprecation.md +++ /dev/null @@ -1,4 +0,0 @@ -## Configuring Contour with a GatewayClass controller name is no longer supported - -Contour can no longer be configured with a GatewayClass controller name (gateway.controllerName in the config file or ContourConfiguration CRD), as the config field has been removed. -Instead, either use a specific Gateway reference (gateway.gatewayRef), or use the Gateway provisioner. diff --git a/changelogs/unreleased/6146-skriss-deprecation.md b/changelogs/unreleased/6146-skriss-deprecation.md deleted file mode 100644 index 3390cab7e85..00000000000 --- a/changelogs/unreleased/6146-skriss-deprecation.md +++ /dev/null @@ -1,5 +0,0 @@ -## Contour xDS server implementation is now deprecated - -As of this release, the `contour` xDS server implementation is now deprecated. -Once the go-control-plane based `envoy` xDS server has had sufficient production bake time, the `contour` implementation will be removed from Contour. -Notification of removal will occur at least one release in advance. diff --git a/changelogs/unreleased/6146-skriss-major.md b/changelogs/unreleased/6146-skriss-major.md deleted file mode 100644 index 5340d272fc7..00000000000 --- a/changelogs/unreleased/6146-skriss-major.md +++ /dev/null @@ -1,25 +0,0 @@ -## Default xDS Server Implementation is now Envoy - -As of this release, Contour now uses the `envoy` xDS server implementation by default. -This xDS server implementation is based on Envoy's [go-control-plane project](https://github.com/envoyproxy/go-control-plane) and will eventually be the only supported xDS server implementation in Contour. -This change is expected to be transparent to users. - -### I'm seeing issues after upgrading, how to I revert to the contour xDS server? - -If you encounter any issues, you can easily revert to the `contour` xDS server with the following configuration: - -(if using Contour config file) -```yaml -server: - xds-server-type: contour -``` - -(if using ContourConfiguration CRD -```yaml -... -spec: - xdsServer: - type: contour -``` - -You will need to restart Contour for the changes to take effect. diff --git a/changelogs/unreleased/6149-izturn-deprecation.md b/changelogs/unreleased/6149-izturn-deprecation.md deleted file mode 100644 index 2566d916f15..00000000000 --- a/changelogs/unreleased/6149-izturn-deprecation.md +++ /dev/null @@ -1,3 +0,0 @@ -## Use of Endpoints API is deprecated - -Contour now uses the EndpointSlices API by default, and its usage of the Endpoints API is deprecated as of this release. Support for Endpoints, and the associated `useEndpointSlices` feature flag, will be removed in a future release. \ No newline at end of file diff --git a/changelogs/unreleased/6149-izturn-minor.md b/changelogs/unreleased/6149-izturn-minor.md deleted file mode 100644 index 9fa8122d473..00000000000 --- a/changelogs/unreleased/6149-izturn-minor.md +++ /dev/null @@ -1,5 +0,0 @@ - -## Use EndpointSlices by default - -Contour now uses the Kubernetes EndpointSlices API by default to determine the endpoints to configure Envoy, instead of the Endpoints API. -Note: if you need to continue using the Endpoints API, you can disable the feature flag via `featureFlags: ["useEndpointSlices=false"]` in the Contour config file or ContourConfiguration CRD. diff --git a/changelogs/unreleased/6153-sunjayBhatia-major.md b/changelogs/unreleased/6153-sunjayBhatia-major.md deleted file mode 100644 index c35c6e25bbe..00000000000 --- a/changelogs/unreleased/6153-sunjayBhatia-major.md +++ /dev/null @@ -1,5 +0,0 @@ -## Gateway API: Inform on v1 types - -Contour no longer informs on v1beta1 resources that have graduated to v1. -This includes the "core" resources GatewayClass, Gateway, and HTTPRoute. -This means that users should ensure they have updated CRDs to Gateway API v1.0.0 or newer, which introduced the v1 version with compatibility with v1beta1. diff --git a/changelogs/unreleased/6185-sunjayBhatia-small.md b/changelogs/unreleased/6185-sunjayBhatia-small.md deleted file mode 100644 index ef1bb0a8169..00000000000 --- a/changelogs/unreleased/6185-sunjayBhatia-small.md +++ /dev/null @@ -1 +0,0 @@ -Fix data race in BackendTLSPolicy status update logic. diff --git a/changelogs/unreleased/6188-lubronzhan-minor.md b/changelogs/unreleased/6188-lubronzhan-minor.md deleted file mode 100644 index 73240450ad5..00000000000 --- a/changelogs/unreleased/6188-lubronzhan-minor.md +++ /dev/null @@ -1,10 +0,0 @@ -## Gateway API: handle Route conflicts with HTTPRoute.Matches - -It's possible that multiple HTTPRoutes will define the same Match conditions. In this case the following logic is applied to resolve the conflict: - -- 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”. -- The Route appearing first in alphabetical order (namespace/name) for example, foo/bar is given precedence over foo/baz. - -With above ordering, any HTTPRoute that ranks lower, will be marked with below conditions accordionly -1. If only partial rules under this HTTPRoute are conflicted, it's marked with `Accepted: True` and `PartiallyInvalid: true` Conditions and Reason: `RuleMatchPartiallyConflict`. -2. If all the rules under this HTTPRoute are conflicted, it's marked with `Accepted: False` Condition and Reason `RuleMatchConflict`. diff --git a/changelogs/unreleased/6230-yangyy93-small.md b/changelogs/unreleased/6230-yangyy93-small.md deleted file mode 100644 index bc07a9b327f..00000000000 --- a/changelogs/unreleased/6230-yangyy93-small.md +++ /dev/null @@ -1 +0,0 @@ -Fix for specifying a health check port with an ExternalName Service. diff --git a/changelogs/unreleased/6246-skriss-small.md b/changelogs/unreleased/6246-skriss-small.md deleted file mode 100644 index be0bf9fa7d1..00000000000 --- a/changelogs/unreleased/6246-skriss-small.md +++ /dev/null @@ -1 +0,0 @@ -Updates the example `envoyproxy/ratelimit` image tag to `19f2079f`, for multi-arch support and other improvements. \ No newline at end of file diff --git a/changelogs/unreleased/6250-skriss-small.md b/changelogs/unreleased/6250-skriss-small.md deleted file mode 100644 index 9a93efe2d60..00000000000 --- a/changelogs/unreleased/6250-skriss-small.md +++ /dev/null @@ -1 +0,0 @@ -In the `envoy` go-control-plane xDS server, use a separate snapshot cache for Endpoints, to minimize the amount of unnecessary xDS traffic generated. \ No newline at end of file diff --git a/changelogs/unreleased/6271-SamMHD-minor.md b/changelogs/unreleased/6271-SamMHD-minor.md deleted file mode 100644 index 43cb5cc26f4..00000000000 --- a/changelogs/unreleased/6271-SamMHD-minor.md +++ /dev/null @@ -1,4 +0,0 @@ -## Spawn Upstream Span is now enabled in tracing - -As described in [Envoy documentations](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-tracing), ```spawn_upstream_span``` should be true when envoy is working as an independent proxy and from now on contour tracing spans will show up as a parent span to upstream spans. - diff --git a/changelogs/unreleased/6295-tsaarni-small.md b/changelogs/unreleased/6295-tsaarni-small.md deleted file mode 100644 index 9747391898f..00000000000 --- a/changelogs/unreleased/6295-tsaarni-small.md +++ /dev/null @@ -1 +0,0 @@ -If there were no relevant resources for Contour in the watched namespaces during the startup of a follower instance of Contour, it did not reach a ready state. diff --git a/changelogs/unreleased/6297-rajatvig-small.md b/changelogs/unreleased/6297-rajatvig-small.md deleted file mode 100644 index b62c0e8b429..00000000000 --- a/changelogs/unreleased/6297-rajatvig-small.md +++ /dev/null @@ -1 +0,0 @@ -Added support for enabling circuit breaker statistics tracking. diff --git a/changelogs/unreleased/6327-skriss-small.md b/changelogs/unreleased/6327-skriss-small.md deleted file mode 100644 index e91daa78e17..00000000000 --- a/changelogs/unreleased/6327-skriss-small.md +++ /dev/null @@ -1 +0,0 @@ -Updates to Go 1.22.2. See the [Go release notes](https://go.dev/doc/devel/release#go1.22.minor) for more information. diff --git a/changelogs/unreleased/6335-skriss-small.md b/changelogs/unreleased/6335-skriss-small.md deleted file mode 100644 index 35dfdfe90d8..00000000000 --- a/changelogs/unreleased/6335-skriss-small.md +++ /dev/null @@ -1 +0,0 @@ -Gateway API: add support for HTTPRoute's Timeouts.BackendRequest field. \ No newline at end of file diff --git a/changelogs/unreleased/6353-tico88612-small.md b/changelogs/unreleased/6353-tico88612-small.md deleted file mode 100644 index 533d34e8979..00000000000 --- a/changelogs/unreleased/6353-tico88612-small.md +++ /dev/null @@ -1 +0,0 @@ -Updates Envoy to v1.30.1. See the v1.30.0 release notes [here](https://www.envoyproxy.io/docs/envoy/v1.30.1/version_history/v1.30/v1.30.0) and the v1.30.1 release notes [here](https://www.envoyproxy.io/docs/envoy/v1.30.1/version_history/v1.30/v1.30.1). diff --git a/changelogs/unreleased/6375-skriss-small.md b/changelogs/unreleased/6375-skriss-small.md deleted file mode 100644 index 6fdfd27f054..00000000000 --- a/changelogs/unreleased/6375-skriss-small.md +++ /dev/null @@ -1 +0,0 @@ -Gateway API: a timeout value of `0s` disables the timeout. \ No newline at end of file diff --git a/changelogs/unreleased/6413-sunjayBhatia-small.md b/changelogs/unreleased/6413-sunjayBhatia-small.md deleted file mode 100644 index 192aa97295b..00000000000 --- a/changelogs/unreleased/6413-sunjayBhatia-small.md +++ /dev/null @@ -1 +0,0 @@ -Fix provisioner to use separate `--disable-feature` flags on Contour Deployment for each disabled feature. Previously a comma separated list was passed which was incorrect. diff --git a/site/config.yaml b/site/config.yaml index 53a7502e048..7abc262ada1 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -29,7 +29,7 @@ params: github_url: "https://github.com/projectcontour/contour" github_raw_url: "https://raw.githubusercontent.com/projectcontour/contour" slack_url: "https://kubernetes.slack.com/messages/contour" - latest_version: "1.28" + latest_version: "1.29" use_advanced_docs: true docs_right_sidebar: true docs_search: true @@ -39,6 +39,7 @@ params: docs_versioning: true docs_versions: - main + - "1.29" - "1.28" - "1.27" - "1.26" diff --git a/site/content/docs/1.29/_index.md b/site/content/docs/1.29/_index.md new file mode 100644 index 00000000000..7e76e77d6df --- /dev/null +++ b/site/content/docs/1.29/_index.md @@ -0,0 +1,48 @@ +--- +cascade: + layout: docs + version: "1.29" + branch: release-1.29 +--- + +## Overview +Contour is an Ingress controller for Kubernetes that works by deploying the [Envoy proxy][1] as a reverse proxy and load balancer. +Contour supports dynamic configuration updates out of the box while maintaining a lightweight profile. + +## Philosophy +- Follow an opinionated approach which allows us to better serve most users +- Design Contour to serve both the cluster administrator and the application developer +- Use our experience with ingress to define reasonable defaults for both cluster administrators and application developers. +- Meet users where they are by understanding and adapting Contour to their use cases + +See the full [Contour Philosophy][8] page. + +## Why Contour? +Contour bridges other solution gaps in several ways: +- Dynamically update the ingress configuration with minimal dropped connections +- Safely support multiple types of ingress config in multi-team Kubernetes clusters + - [Ingress/v1][10] + - [HTTPProxy (Contour custom resource)][2] + - [Gateway API][9] +- Cleanly integrate with the Kubernetes object model + +## Prerequisites +Contour is tested with Kubernetes clusters running version [1.21 and later][4]. + +## Get started +Getting started with Contour is as simple as one command. +See the [Getting Started][3] document. + +## Troubleshooting +If you encounter issues review the [troubleshooting][5] page, [file an issue][6], or talk to us on the [#contour channel][7] on Kubernetes slack. + +[1]: https://www.envoyproxy.io/ +[2]: config/fundamentals.md +[3]: /getting-started +[4]: /resources/compatibility-matrix.md +[5]: /docs/main/troubleshooting +[6]: https://github.com/projectcontour/contour/issues +[7]: https://kubernetes.slack.com/messages/contour +[8]: /resources/philosophy +[9]: guides/gateway-api +[10]: /docs/{{< param version >}}/config/ingress diff --git a/site/content/docs/1.29/architecture.md b/site/content/docs/1.29/architecture.md new file mode 100644 index 00000000000..b29cb409d39 --- /dev/null +++ b/site/content/docs/1.29/architecture.md @@ -0,0 +1,74 @@ +# Contour Architecture + +The Contour Ingress controller is a collaboration between: + +* Envoy, which provides the high performance reverse proxy. +* Contour, which acts as a management server for Envoy and provides it with configuration. + +These containers are deployed separately, Contour as a Deployment and Envoy as a Kubernetes Daemonset or Deployment, although other configurations are possible. + +In the Envoy Pods, Contour runs as an initcontainer in `bootstrap` mode and writes an Envoy bootstrap configuration to a temporary volume. +This volume is passed to the Envoy container and directs Envoy to treat Contour as its [management server][1]. + +After initialization is complete, the Envoy container starts, retrieves the bootstrap configuration written by Contour's `bootstrap` mode, and establishes a GRPC session with Contour to receive configuration. + +Envoy will gracefully retry if the management server is unavailable, which removes any container startup ordering issues. + +Contour is a client of the Kubernetes API. +Contour watches Ingress, HTTPProxy, Gateway API, Secret, Service, and Endpoint objects, and acts as the management server for its Envoy sibling by translating its cache of objects into the relevant JSON stanzas: Service objects for CDS, Ingress for RDS, Endpoint objects for EDS, and so on). + +The transfer of information from Kubernetes to Contour is by watching the Kubernetes API utilizing [controller-runtime][4] primitives. + +Kubernetes readiness probes are configured to check whether Envoy is ready to accept connections. +The Envoy readiness probe sends GET requests to `/ready` in Envoy's administration endpoint. + +For Contour, a liveness probe checks the `/healthz` running on the Pod's metrics port. +Readiness probe is a check that Contour can access the Kubernetes API. + +## Architectural Overview +Below are a couple of high level architectural diagrams of how Contour works inside a Kubernetes cluster as well as showing the data path of a request to a backend pod. + +A request to `projectcontour.io/blog` gets routed via a load balancer to an instance of an Envoy proxy which then sends the request to a pod. + +![architectural overview][2] + +Following is a diagram of how Contour and Envoy are deployed in a Kubernetes cluster. + +### Kubernetes API Server + +The following API objects are watched: +- Services +- Endpoints +- Secrets +- Ingress +- HTTPProxy +- Gateway API (Optional) + +### Contour Deployment + +Contour is deployed in the cluster using a Kubernetes Deployment. +It has built-in leader election which is responsible for updating httproxy/ingress/gateway api resources via Kube API server. +All instances are able to serve xDS configuration to any Envoy instance, but only the leader can write status back to the API server. + +The data being served from contour instances are eventually consistent in an HA based deployment. +However HA mode is operationally scalable when you have high request rate from envoy to contour as requests are loadbalanced among contour instances. +This also helps availability zone /data center degradation events as your service continue to function. + +### Envoy Deployment + +Envoy can be deployed in two different models, as a Kubernetes Daemonset or as a Kubernetes Deployment. + +Daemonset is the standard deployment model where a single instance of Envoy is deployed per Kubernetes Node. +This allows for simple Envoy pod distribution across the cluster as well as being able to expose Envoy using `hostPorts` to improve network performance. +One potential downside of this deployment model is when a node is removed from the cluster (e.g. on a cluster scale down, etc) then the configured `preStop` hooks are not available so connections can be dropped. +This is a limitation that applies to any Daemonset in Kubernetes. + +An alternative Envoy deployment model is utilizing a Kubernetes Deployment with a configured `podAntiAffinity` which attempts to mirror the Daemonset deployment model. +A benefit of this model compared to the Daemonset version is when a node is removed from the cluster, the proper shutdown events are available so connections can be cleanly drained from Envoy before terminating. + +![architectural overview 2][3] + +[1]: https://www.envoyproxy.io/docs/envoy/v1.13.0/api-docs/xds_protocol +[2]: ../img/archoverview.png +[3]: ../img/contour_deployment_in_k8s.png +[4]: https://github.com/kubernetes-sigs/controller-runtime diff --git a/site/content/docs/1.29/config/access-logging.md b/site/content/docs/1.29/config/access-logging.md new file mode 100644 index 00000000000..0c5b6e1583c --- /dev/null +++ b/site/content/docs/1.29/config/access-logging.md @@ -0,0 +1,148 @@ +# Access Logging + +## Overview + +Contour allows you to control Envoy's access logging. +By default, HTTP and HTTPS access logs are written to `/dev/stdout` by the Envoy containers and look like following: + +``` +[2021-04-14T16:36:00.361Z] "GET /foo HTTP/1.1" 200 - 0 463 6 3 "-" "HTTPie/1.0.3" "837aa8dc-344f-4faa-b7d5-c9cce1028519" "localhost:8080" "127.0.0.1:8081" +``` + +The detailed description of each field can be found in [Envoy access logging documentation][7]. + + +## Customizing Access Log Destination + +You can change the destination file where the access log is written by using Contour [command line parameters][1] `--envoy-http-access-log` and `--envoy-https-access-log`. + +## Customizing Access Log Format + +The access log can take two different formats, both can be customized + +* Text based access logs, like shown in the example above. +* Structured JSON logging. + +### Text Based Access Logging + +Ensure that you have selected `envoy` as the access log format. +Note that this is the default format if the parameters are not given. + +- Add `--accesslog-format=envoy` to your Contour startup line, or +- Add `accesslog-format: envoy` to your configuration file. + +Customize the access log format by defining `accesslog-format-string` in your configuration file. + +```yaml +accesslog-format-string: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\"\n" +``` +After restarting Contour and successful validation of the configuration, the new format will take effect in a short while. + +Refer to [Envoy access logging documentation][7] for the description of the command operators, and note that the format string needs to end in a linefeed `\n`. + +### Structured JSON Logging + +Contour allows you to choose from a set of JSON fields that will be expanded into Envoy templates and sent to Envoy. +There is a default set of fields if you enable JSON logging, and you may customize which fields you log. + +The list of available fields are discoverable in the following objects: +- [jsonFields][2] are fields that have built in mappings to commonly used envoy operators. +- [envoySimpleOperators][3] are the names of simple envoy operators that don't require arguments, they are case-insensitive when configured. +- [envoyComplexOperators][4] are the names of complex envoy operators that require arguments. + +The default list of fields is available at [DefaultAccessLogJSONFields][5]. + +#### Enabling the Feature + +To enable the feature you have two options: + +- Add `--accesslog-format=json` to your Contour startup line. +- Add `accesslog-format: json` to your configuration file. + +Without any further customization, the [default fields][5] will be used. + +#### Customizing Logged Fields + +To customize the logged fields, add a `json-fields` list of strings to your configuration file. +If the `json-fields` key is not specified, the [default fields][5] will be configured. + +To use a value from [jsonFields][2] or [envoySimpleOperators][3], simply include the name of the value in the list of strings. +The jsonFields are case-sensitive, but envoySimpleOperators are not. + +To use [envoyComplexOperators][4] or to use alternative field names, specify strings as key/value pairs like `"fieldName=%OPERATOR(...)%"`. + +Unknown field names in non key/value fields will result in validation errors, as will unknown Envoy operators in key/value fields. +Note that the `DYNAMIC_METADATA` and `FILTER_STATE` Envoy logging operators are not supported at this time due to the complexity of their validation. + +See the [example config file][6] to see this used in context. + +#### Omitting Logs with Empty Values + +Contour automatically omits empty fields in Envoy JSON access logs, enhancing clarity and delivering more concise and relevant log outputs by default. + +#### Sample Configuration File + +Here is a sample config: + +```yaml +accesslog-format: json +json-fields: + - "@timestamp" + - "authority" + - "bytes_received" + - "bytes_sent" + - "customer_id=%REQ(X-CUSTOMER-ID)%" + - "downstream_local_address" + - "downstream_remote_address" + - "duration" + - "method" + - "path" + - "protocol" + - "request_id" + - "requested_server_name" + - "response_code" + - "response_flags" + - "uber_trace_id" + - "upstream_cluster" + - "upstream_host" + - "upstream_local_address" + - "upstream_service_time" + - "user_agent" + - "x_forwarded_for" +``` + +### Logging the route source + +Contour can log the kind, namespace and name of the Kubernetes resource that generated the route for a given access log entry. + +For text-based access logging, the following command operators can be used: +- `%METADATA(ROUTE:envoy.access_loggers.file:io.projectcontour.kind)%` +- `%METADATA(ROUTE:envoy.access_loggers.file:io.projectcontour.namespace)%` +- `%METADATA(ROUTE:envoy.access_loggers.file:io.projectcontour.name)%` + +For JSON access logging, the following fields can be added (these are Contour-specific aliases to the above command operators): +- `contour_config_kind` +- `contour_config_namespace` +- `contour_config_name` + +## Using Access Log Formatter Extensions + +Envoy allows implementing custom access log command operators as extensions. +Following extensions are supported by Contour: + +| Command operator | Description | +|------------------|-------------| +| [REQ_WITHOUT_QUERY][8] | Works the same way as REQ except that it will remove the query string. It is used to avoid logging any sensitive information into the access log. | +| [METADATA][9] | Prints all types of metadata. | + + + +[1]: ../configuration#serve-flags +[2]: https://github.com/search?q=%22var+jsonFields%22+repo%3Aprojectcontour%2Fcontour+path%3Aapis&type=code +[3]: https://github.com/search?q=%22var+envoySimpleOperators%22+repo%3Aprojectcontour%2Fcontour+path%3Aapis&type=code +[4]: https://github.com/search?q=%22var+envoyComplexOperators%22+repo%3Aprojectcontour%2Fcontour+path%3Aapis&type=code +[5]: https://github.com/search?q=%22var+DefaultAccessLogJSONFields%22+repo%3Aprojectcontour%2Fcontour+path%3Aapis&type=code +[6]: {{< param github_url >}}/tree/{{< param latest_version >}}/examples/contour/01-contour-config.yaml +[7]: https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage +[8]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/formatter/req_without_query/v3/req_without_query.proto +[9]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/formatter/metadata/v3/metadata.proto \ No newline at end of file diff --git a/site/content/docs/1.29/config/annotations.md b/site/content/docs/1.29/config/annotations.md new file mode 100644 index 00000000000..4d805f8f0f1 --- /dev/null +++ b/site/content/docs/1.29/config/annotations.md @@ -0,0 +1,98 @@ +# Annotations Reference + + + +Annotations are used in Ingress Controllers to configure features that are not covered by the Kubernetes Ingress API. + +Some of the features that have been historically configured via annotations are supported as first-class features in Contour's [HTTPProxy API][15], which provides a more robust configuration interface over annotations. + +However, Contour still supports a number of annotations on the Ingress resources. + +## Standard Kubernetes Ingress annotations + +The following Kubernetes annotations are supported on `Ingress` objects: + +### Ingress Class + +The Ingress class annotation can be used to specify which Ingress controller should serve a particular Ingress object. +This annotation may be specified as the standard `kubernetes.io/ingress.class` or a Contour-specific `projectcontour.io/ingress.class`. +In both cases, they will behave as follows, by default: + +* If not set, then all Ingress controllers serve the Ingress. +* If specified as `kubernetes.io/ingress.class: contour`, then Contour serves the Ingress. +* If any other value, Contour ignores the Ingress definition. + +You can override the default class `contour` by providing the `--ingress-class-name` flag to Contour. +This can be useful while you are migrating from another controller, or if you need multiple instances of Contour. +If you do this, the behavior is as follows: +* If the annotation is not set, Contour will ignore the Ingress. +* If the annotation is set to any value other than the one passed to the `--ingress-class-name` flag, Contour will ignore the Ingress. +* If the annotation matches the value that you passed to `--ingress-class-name` flag, Contour will serve the Ingress. + +This same logic applies for these annotations on HTTPProxy objects. + +_Note: Both `Ingress` and `HTTPProxy` now have an `IngressClassName` field in their spec. Going forward this is the preferred way to specify an ingress class, rather than using an annotation. If both the annotation and the spec field are specified on an object, the annotation takes preference for backwards compatibility._ + +_Note: The `--ingress-class-name` value can be a comma-separated list of class names to match against. Contour will serve the Ingress or HTTPProxy if the annotation or IngressClassName matches any of the specified class name values. + +### Other annotations + + - `ingress.kubernetes.io/force-ssl-redirect`: Requires TLS/SSL for the Ingress to Envoy by setting the [Envoy virtual host option require_tls][16]. + - `kubernetes.io/ingress.allow-http`: Instructs Contour to not create an Envoy HTTP route for the virtual host. The Ingress exists only for HTTPS requests. Specify `"false"` for Envoy to mark the endpoint as HTTPS only. All other values are ignored. + +The `ingress.kubernetes.io/force-ssl-redirect` annotation takes precedence over `kubernetes.io/ingress.allow-http`. If they are set to `"true"` and `"false"` respectively, Contour *will* create an Envoy HTTP route for the Virtual host, and set the `require_tls` virtual host option. + +## Contour specific Ingress annotations + + - `projectcontour.io/ingress.class`: The Ingress class that should interpret and serve the Ingress. See the [main Ingress class annotation section](#ingress-class) for more details. + - `projectcontour.io/num-retries`: [The maximum number of retries][1] Envoy should make before abandoning and returning an error to the client. Applies only if `projectcontour.io/retry-on` is specified. Set to -1 to disable retries. + - `projectcontour.io/per-try-timeout`: [The timeout per retry attempt][2], if there should be one. Applies only if `projectcontour.io/retry-on` is specified. + - `projectcontour.io/response-timeout`: [The Envoy HTTP route timeout][3], specified as a [golang duration][4]. By default, Envoy has a 15 second timeout for a backend service to respond. Set this to `infinity` to specify that Envoy should never timeout the connection to the backend. Note that the value `0s` / zero has special semantics for Envoy. + - `projectcontour.io/retry-on`: [The conditions for Envoy to retry a request][5]. See also [possible values and their meanings for `retry-on`][6]. + - `projectcontour.io/tls-minimum-protocol-version`: [The minimum TLS protocol version][7] the TLS listener should support. Valid options are `1.3`, `1.2` (default). + - `projectcontour.io/tls-maximum-protocol-version`: [The maximum TLS protocol version][7] the TLS listener should support. Valid options are `1.2`, `1.3` (default). + - `projectcontour.io/websocket-routes`: [The routes supporting websocket protocol][8], the annotation value contains a list of route paths separated by a comma that must match with the ones defined in the `Ingress` definition. Defaults to Envoy's default behavior which is `use_websocket` to `false`. + - `projectcontour.io/tls-cert-namespace`: The namespace where all TLS secrets of this Ingress are searched. This is necessary to use [TLS Certificate Delegation][18] with Ingress v1 because the slash notation (ex: different-ns/app-cert) used by HTTPProxy and Ingress v1beta1 is not accepted. See [this issue][19] for details. + +## Contour specific Service annotations + +A [Kubernetes Service][9] maps to an [Envoy Cluster][10]. Envoy clusters have many settings to control specific behaviors. These annotations allow access to some of those settings. + +- `projectcontour.io/max-connections`: [The maximum number of connections][11] that a single Envoy instance allows to the Kubernetes Service; defaults to 1024. +- `projectcontour.io/max-pending-requests`: [The maximum number of pending requests][13] that a single Envoy instance allows to the Kubernetes Service; defaults to 1024. +- `projectcontour.io/max-requests`: [The maximum parallel requests][13] a single Envoy instance allows to the Kubernetes Service; defaults to 1024 +- `projectcontour.io/max-retries`: [The maximum number of parallel retries][14] a single Envoy instance allows to the Kubernetes Service; defaults to 3. This is independent of the per-Kubernetes Ingress number of retries (`projectcontour.io/num-retries`) and retry-on (`projectcontour.io/retry-on`), which control whether retries are attempted and how many times a single request can retry. +- `projectcontour.io/per-host-max-connections`: [The maximum number of connections][20] that a single Envoy instance allows to an individual Kubernetes Service endpoint; no default (unlimited). +- `projectcontour.io/upstream-protocol.{protocol}` : The protocol used to proxy requests to the upstream service. + The annotation value contains a comma-separated list of port names and/or numbers that must match with the ones defined in the `Service` definition. + This value can also be specified in the `spec.routes.services[].protocol` field on the HTTPProxy object, where it takes precedence over the Service annotation. + Supported protocol names are: `h2`, `h2c`, and `tls`: + - The `tls` protocol allows for requests which terminate at Envoy to proxy via TLS to the upstream. + This protocol should be used for HTTP/1.1 services over TLS. + _Note that validating the upstream TLS certificate requires additionally setting the [validation][17] field._ + - The `h2` protocol proxies requests to the upstream using HTTP/2 over TLS. + - The `h2c` protocol proxies requests to the upstream using cleartext HTTP/2. + +## Contour specific HTTPProxy annotations +- `projectcontour.io/ingress.class`: The Ingress class that should interpret and serve the HTTPProxy. See the [main Ingress class annotation section](#ingress-class) for more details. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries +[2]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-retrypolicy-retry-on +[3]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-timeout +[4]: https://golang.org/pkg/time/#ParseDuration +[5]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-retrypolicy-retry-on +[6]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-retry-on +[7]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto.html#extensions-transport-sockets-tls-v3-tlsparameters +[8]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-upgrade-configs +[9]: https://kubernetes.io/docs/concepts/services-networking/service/ +[10]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/intro/terminology +[11]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto#envoy-v3-api-field-config-cluster-v3-circuitbreakers-thresholds-max-connections +[12]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto#envoy-v3-api-field-config-cluster-v3-circuitbreakers-thresholds-max-pending-requests +[13]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto#envoy-v3-api-field-config-cluster-v3-circuitbreakers-thresholds-max-requests +[14]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto#envoy-v3-api-field-config-cluster-v3-circuitbreakers-thresholds-max-retries +[15]: fundamentals.md +[16]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-virtualhost-require-tls +[17]: api/#projectcontour.io/v1.UpstreamValidation +[18]: ../config/tls-delegation/ +[19]: https://github.com/projectcontour/contour/issues/3544 +[20]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto#envoy-v3-api-field-config-cluster-v3-circuitbreakers-per-host-thresholds \ No newline at end of file diff --git a/site/content/docs/1.29/config/api-reference.html b/site/content/docs/1.29/config/api-reference.html new file mode 100644 index 00000000000..b9ba8f7400d --- /dev/null +++ b/site/content/docs/1.29/config/api-reference.html @@ -0,0 +1,9109 @@ +

Packages:

+ +

projectcontour.io/v1

+

+

Package v1 holds the specification for the projectcontour.io Custom Resource Definitions (CRDs).

+

In building this CRD, we’ve inadvertently overloaded the word “Condition”, so we’ve tried to make +this spec clear as to which types of condition are which.

+

MatchConditions are used by Routes and Includes to specify rules to match requests against for either +routing or inclusion.

+

DetailedConditions are used in the Status of these objects to hold information about the relevant +state of the object and the world around it.

+

SubConditions are used underneath DetailedConditions to give more detail to errors or warnings.

+

+Resource Types: + +

HTTPProxy +

+

+

HTTPProxy is an Ingress CRD specification.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +projectcontour.io/v1 + +
+kind
+string +
HTTPProxy
+metadata +
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec +
+ + +HTTPProxySpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+virtualhost +
+ + +VirtualHost + + +
+(Optional) +

Virtualhost appears at most once. If it is present, the object is considered +to be a “root” HTTPProxy.

+
+routes +
+ + +[]Route + + +
+(Optional) +

Routes are the ingress routes. If TCPProxy is present, Routes is ignored.

+
+tcpproxy +
+ + +TCPProxy + + +
+(Optional) +

TCPProxy holds TCP proxy information.

+
+includes +
+ + +[]Include + + +
+(Optional) +

Includes allow for specific routing configuration to be included from another HTTPProxy, +possibly in another namespace.

+
+ingressClassName +
+ +string + +
+(Optional) +

IngressClassName optionally specifies the ingress class to use for this +HTTPProxy. This replaces the deprecated kubernetes.io/ingress.class +annotation. For backwards compatibility, when that annotation is set, it +is given precedence over this field.

+
+
+status +
+ + +HTTPProxyStatus + + +
+(Optional) +

Status is a container for computed information about the HTTPProxy.

+
+

TLSCertificateDelegation +

+

+

TLSCertificateDelegation is an TLS Certificate Delegation CRD specification. +See design/tls-certificate-delegation.md for details.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +projectcontour.io/v1 + +
+kind
+string +
TLSCertificateDelegation
+metadata +
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec +
+ + +TLSCertificateDelegationSpec + + +
+
+
+ + + + + +
+delegations +
+ + +[]CertificateDelegation + + +
+
+
+status +
+ + +TLSCertificateDelegationStatus + + +
+(Optional) +
+

AuthorizationPolicy +

+

+(Appears on: +AuthorizationServer, +Route) +

+

+

AuthorizationPolicy modifies how client requests are authenticated.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+disabled +
+ +bool + +
+(Optional) +

When true, this field disables client request authentication +for the scope of the policy.

+
+context +
+ +map[string]string + +
+(Optional) +

Context is a set of key/value pairs that are sent to the +authentication server in the check request. If a context +is provided at an enclosing scope, the entries are merged +such that the inner scope overrides matching keys from the +outer scope.

+
+

AuthorizationServer +

+

+(Appears on: +VirtualHost, +ContourConfigurationSpec) +

+

+

AuthorizationServer configures an external server to authenticate +client requests. The external server must implement the v3 Envoy +external authorization GRPC protocol (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto).

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+extensionRef +
+ + +ExtensionServiceReference + + +
+(Optional) +

ExtensionServiceRef specifies the extension resource that will authorize client requests.

+
+authPolicy +
+ + +AuthorizationPolicy + + +
+(Optional) +

AuthPolicy sets a default authorization policy for client requests. +This policy will be used unless overridden by individual routes.

+
+responseTimeout +
+ +string + +
+(Optional) +

ResponseTimeout configures maximum time to wait for a check response from the authorization server. +Timeout durations are expressed in the Go Duration format. +Valid time units are “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”. +The string “infinity” is also a valid input and specifies no timeout.

+
+failOpen +
+ +bool + +
+(Optional) +

If FailOpen is true, the client request is forwarded to the upstream service +even if the authorization server fails to respond. This field should not be +set in most cases. It is intended for use only while migrating applications +from internal authorization to Contour external authorization.

+
+withRequestBody +
+ + +AuthorizationServerBufferSettings + + +
+(Optional) +

WithRequestBody specifies configuration for sending the client request’s body to authorization server.

+
+

AuthorizationServerBufferSettings +

+

+(Appears on: +AuthorizationServer) +

+

+

AuthorizationServerBufferSettings enables ExtAuthz filter to buffer client request data and send it as part of authorization request

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+maxRequestBytes +
+ +uint32 + +
+(Optional) +

MaxRequestBytes sets the maximum size of message body ExtAuthz filter will hold in-memory.

+
+allowPartialMessage +
+ +bool + +
+(Optional) +

If AllowPartialMessage is true, then Envoy will buffer the body until MaxRequestBytes are reached.

+
+packAsBytes +
+ +bool + +
+(Optional) +

If PackAsBytes is true, the body sent to Authorization Server is in raw bytes.

+
+

CORSHeaderValue +(string alias)

+

+(Appears on: +CORSPolicy) +

+

+

CORSHeaderValue specifies the value of the string headers returned by a cross-domain request.

+

+

CORSPolicy +

+

+(Appears on: +VirtualHost) +

+

+

CORSPolicy allows setting the CORS policy

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+allowCredentials +
+ +bool + +
+(Optional) +

Specifies whether the resource allows credentials.

+
+allowOrigin +
+ +[]string + +
+

AllowOrigin specifies the origins that will be allowed to do CORS requests. +Allowed values include “*” which signifies any origin is allowed, an exact +origin of the form “scheme://host[:port]” (where port is optional), or a valid +regex pattern. +Note that regex patterns are validated and a simple “glob” pattern (e.g. *.foo.com) +will be rejected or produce unexpected matches when applied as a regex.

+
+allowMethods +
+ + +[]CORSHeaderValue + + +
+

AllowMethods specifies the content for the access-control-allow-methods header.

+
+allowHeaders +
+ + +[]CORSHeaderValue + + +
+(Optional) +

AllowHeaders specifies the content for the access-control-allow-headers header.

+
+exposeHeaders +
+ + +[]CORSHeaderValue + + +
+(Optional) +

ExposeHeaders Specifies the content for the access-control-expose-headers header.

+
+maxAge +
+ +string + +
+(Optional) +

MaxAge indicates for how long the results of a preflight request can be cached. +MaxAge durations are expressed in the Go Duration format. +Valid time units are “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”. +Only positive values are allowed while 0 disables the cache requiring a preflight OPTIONS +check for all cross-origin requests.

+
+allowPrivateNetwork +
+ +bool + +
+

AllowPrivateNetwork specifies whether to allow private network requests. +See https://developer.chrome.com/blog/private-network-access-preflight.

+
+

CertificateDelegation +

+

+(Appears on: +TLSCertificateDelegationSpec) +

+

+

CertificateDelegation maps the authority to reference a secret +in the current namespace to a set of namespaces.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+secretName +
+ +string + +
+

required, the name of a secret in the current namespace.

+
+targetNamespaces +
+ +[]string + +
+

required, the namespaces the authority to reference the +secret will be delegated to. +If TargetNamespaces is nil or empty, the CertificateDelegation’ +is ignored. If the TargetNamespace list contains the character, “*” +the secret will be delegated to all namespaces.

+
+

ClientCertificateDetails +

+

+(Appears on: +DownstreamValidation) +

+

+

ClientCertificateDetails defines which parts of the client certificate will be forwarded.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+subject +
+ +bool + +
+(Optional) +

Subject of the client cert.

+
+cert +
+ +bool + +
+(Optional) +

Client cert in URL encoded PEM format.

+
+chain +
+ +bool + +
+(Optional) +

Client cert chain (including the leaf cert) in URL encoded PEM format.

+
+dns +
+ +bool + +
+(Optional) +

DNS type Subject Alternative Names of the client cert.

+
+uri +
+ +bool + +
+(Optional) +

URI type Subject Alternative Name of the client cert.

+
+

CookieDomainRewrite +

+

+(Appears on: +CookieRewritePolicy) +

+

+

+ + + + + + + + + + + + + +
FieldDescription
+value +
+ +string + +
+

Value is the value to rewrite the Domain attribute to. +For now this is required.

+
+

CookiePathRewrite +

+

+(Appears on: +CookieRewritePolicy) +

+

+

+ + + + + + + + + + + + + +
FieldDescription
+value +
+ +string + +
+

Value is the value to rewrite the Path attribute to. +For now this is required.

+
+

CookieRewritePolicy +

+

+(Appears on: +Route, +Service) +

+

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Name is the name of the cookie for which attributes will be rewritten.

+
+pathRewrite +
+ + +CookiePathRewrite + + +
+(Optional) +

PathRewrite enables rewriting the Set-Cookie Path element. +If not set, Path will not be rewritten.

+
+domainRewrite +
+ + +CookieDomainRewrite + + +
+(Optional) +

DomainRewrite enables rewriting the Set-Cookie Domain element. +If not set, Domain will not be rewritten.

+
+secure +
+ +bool + +
+(Optional) +

Secure enables rewriting the Set-Cookie Secure element. +If not set, Secure attribute will not be rewritten.

+
+sameSite +
+ +string + +
+(Optional) +

SameSite enables rewriting the Set-Cookie SameSite element. +If not set, SameSite attribute will not be rewritten.

+
+

DetailedCondition +

+

+(Appears on: +HTTPProxyStatus, +TLSCertificateDelegationStatus, +ContourConfigurationStatus, +ExtensionServiceStatus) +

+

+

DetailedCondition is an extension of the normal Kubernetes conditions, with two extra +fields to hold sub-conditions, which provide more detailed reasons for the state (True or False) +of the condition.

+

errors holds information about sub-conditions which are fatal to that condition and render its state False.

+

warnings holds information about sub-conditions which are not fatal to that condition and do not force the state to be False.

+

Remember that Conditions have a type, a status, and a reason.

+

The type is the type of the condition, the most important one in this CRD set is Valid. +Valid is a positive-polarity condition: when it is status: true there are no problems.

+

In more detail, status: true means that the object is has been ingested into Contour with no errors. +warnings may still be present, and will be indicated in the Reason field. There must be zero entries in the errors +slice in this case.

+

Valid, status: false means that the object has had one or more fatal errors during processing into Contour. +The details of the errors will be present under the errors field. There must be at least one error in the errors +slice if status is false.

+

For DetailedConditions of types other than Valid, the Condition must be in the negative polarity. +When they have status true, there is an error. There must be at least one entry in the errors Subcondition slice. +When they have status false, there are no serious errors, and there must be zero entries in the errors slice. +In either case, there may be entries in the warnings slice.

+

Regardless of the polarity, the reason and message fields must be updated with either the detail of the reason +(if there is one and only one entry in total across both the errors and warnings slices), or +MultipleReasons if there is more than one entry.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+Condition +
+ + +Kubernetes meta/v1.Condition + + +
+

+(Members of Condition are embedded into this type.) +

+
+errors +
+ + +[]SubCondition + + +
+(Optional) +

Errors contains a slice of relevant error subconditions for this object.

+

Subconditions are expected to appear when relevant (when there is a error), and disappear when not relevant. +An empty slice here indicates no errors.

+
+warnings +
+ + +[]SubCondition + + +
+(Optional) +

Warnings contains a slice of relevant warning subconditions for this object.

+

Subconditions are expected to appear when relevant (when there is a warning), and disappear when not relevant. +An empty slice here indicates no warnings.

+
+

DownstreamValidation +

+

+(Appears on: +TLS) +

+

+

DownstreamValidation defines how to verify the client certificate.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+caSecret +
+ +string + +
+(Optional) +

Name of a Kubernetes secret that contains a CA certificate bundle. +The secret must contain key named ca.crt. +The client certificate must validate against the certificates in the bundle. +If specified and SkipClientCertValidation is true, client certificates will +be required on requests. +The name can be optionally prefixed with namespace “namespace/name”. +When cross-namespace reference is used, TLSCertificateDelegation resource must exist in the namespace to grant access to the secret.

+
+skipClientCertValidation +
+ +bool + +
+(Optional) +

SkipClientCertValidation disables downstream client certificate +validation. Defaults to false. This field is intended to be used in +conjunction with external authorization in order to enable the external +authorization server to validate client certificates. When this field +is set to true, client certificates are requested but not verified by +Envoy. If CACertificate is specified, client certificates are required on +requests, but not verified. If external authorization is in use, they are +presented to the external authorization server.

+
+forwardClientCertificate +
+ + +ClientCertificateDetails + + +
+(Optional) +

ForwardClientCertificate adds the selected data from the passed client TLS certificate +to the x-forwarded-client-cert header.

+
+crlSecret +
+ +string + +
+(Optional) +

Name of a Kubernetes opaque secret that contains a concatenated list of PEM encoded CRLs. +The secret must contain key named crl.pem. +This field will be used to verify that a client certificate has not been revoked. +CRLs must be available from all CAs, unless crlOnlyVerifyLeafCert is true. +Large CRL lists are not supported since individual secrets are limited to 1MiB in size. +The name can be optionally prefixed with namespace “namespace/name”. +When cross-namespace reference is used, TLSCertificateDelegation resource must exist in the namespace to grant access to the secret.

+
+crlOnlyVerifyLeafCert +
+ +bool + +
+(Optional) +

If this option is set to true, only the certificate at the end of the +certificate chain will be subject to validation by CRL.

+
+optionalClientCertificate +
+ +bool + +
+(Optional) +

OptionalClientCertificate when set to true will request a client certificate +but allow the connection to continue if the client does not provide one. +If a client certificate is sent, it will be verified according to the +other properties, which includes disabling validation if +SkipClientCertValidation is set. Defaults to false.

+
+

ExtensionServiceReference +

+

+(Appears on: +AuthorizationServer) +

+

+

ExtensionServiceReference names an ExtensionService resource.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion +
+ +string + +
+(Optional) +

API version of the referent. +If this field is not specified, the default “projectcontour.io/v1alpha1” will be used

+
+namespace +
+ +string + +
+(Optional) +

Namespace of the referent. +If this field is not specifies, the namespace of the resource that targets the referent will be used.

+

More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/

+
+name +
+ +string + +
+

Name of the referent.

+

More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names

+
+

Feature +(string alias)

+

+(Appears on: +ContourSettings) +

+

+

+

GenericKeyDescriptor +

+

+(Appears on: +RateLimitDescriptorEntry) +

+

+

GenericKeyDescriptor defines a descriptor entry with a static key and +value.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+key +
+ +string + +
+(Optional) +

Key defines the key of the descriptor entry. If not set, the +key is set to “generic_key”.

+
+value +
+ +string + +
+

Value defines the value of the descriptor entry.

+
+

GlobalRateLimitPolicy +

+

+(Appears on: +RateLimitPolicy, +RateLimitServiceConfig) +

+

+

GlobalRateLimitPolicy defines global rate limiting parameters.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+disabled +
+ +bool + +
+(Optional) +

Disabled configures the HTTPProxy to not use +the default global rate limit policy defined by the Contour configuration.

+
+descriptors +
+ + +[]RateLimitDescriptor + + +
+(Optional) +

Descriptors defines the list of descriptors that will +be generated and sent to the rate limit service. Each +descriptor contains 1+ key-value pair entries.

+
+

HTTPDirectResponsePolicy +

+

+(Appears on: +Route) +

+

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+statusCode +
+ +int + +
+

StatusCode is the HTTP response status to be returned.

+
+body +
+ +string + +
+(Optional) +

Body is the content of the response body. +If this setting is omitted, no body is included in the generated response.

+

Note: Body is not recommended to set too long +otherwise it can have significant resource usage impacts.

+
+

HTTPHealthCheckPolicy +

+

+(Appears on: +Route) +

+

+

HTTPHealthCheckPolicy defines health checks on the upstream service.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+path +
+ +string + +
+

HTTP endpoint used to perform health checks on upstream service

+
+host +
+ +string + +
+

The value of the host header in the HTTP health check request. +If left empty (default value), the name “contour-envoy-healthcheck” +will be used.

+
+intervalSeconds +
+ +int64 + +
+(Optional) +

The interval (seconds) between health checks

+
+timeoutSeconds +
+ +int64 + +
+(Optional) +

The time to wait (seconds) for a health check response

+
+unhealthyThresholdCount +
+ +int64 + +
+(Optional) +

The number of unhealthy health checks required before a host is marked unhealthy

+
+healthyThresholdCount +
+ +int64 + +
+(Optional) +

The number of healthy health checks required before a host is marked healthy

+
+expectedStatuses +
+ + +[]HTTPStatusRange + + +
+(Optional) +

The ranges of HTTP response statuses considered healthy. Follow half-open +semantics, i.e. for each range the start is inclusive and the end is exclusive. +Must be within the range [100,600). If not specified, only a 200 response status +is considered healthy.

+
+

HTTPInternalRedirectPolicy +

+

+(Appears on: +Route) +

+

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+maxInternalRedirects +
+ +uint32 + +
+(Optional) +

MaxInternalRedirects An internal redirect is not handled, unless the number of previous internal +redirects that a downstream request has encountered is lower than this value.

+
+redirectResponseCodes +
+ + +[]RedirectResponseCode + + +
+(Optional) +

RedirectResponseCodes If unspecified, only 302 will be treated as internal redirect. +Only 301, 302, 303, 307 and 308 are valid values.

+
+allowCrossSchemeRedirect +
+ +string + +
+(Optional) +

AllowCrossSchemeRedirect Allow internal redirect to follow a target URI with a different scheme +than the value of x-forwarded-proto. +SafeOnly allows same scheme redirect and safe cross scheme redirect, which means if the downstream +scheme is HTTPS, both HTTPS and HTTP redirect targets are allowed, but if the downstream scheme +is HTTP, only HTTP redirect targets are allowed.

+
+denyRepeatedRouteRedirect +
+ +bool + +
+(Optional) +

If DenyRepeatedRouteRedirect is true, rejects redirect targets that are pointing to a route that has +been followed by a previous redirect from the current route.

+
+

HTTPProxySpec +

+

+(Appears on: +HTTPProxy) +

+

+

HTTPProxySpec defines the spec of the CRD.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+virtualhost +
+ + +VirtualHost + + +
+(Optional) +

Virtualhost appears at most once. If it is present, the object is considered +to be a “root” HTTPProxy.

+
+routes +
+ + +[]Route + + +
+(Optional) +

Routes are the ingress routes. If TCPProxy is present, Routes is ignored.

+
+tcpproxy +
+ + +TCPProxy + + +
+(Optional) +

TCPProxy holds TCP proxy information.

+
+includes +
+ + +[]Include + + +
+(Optional) +

Includes allow for specific routing configuration to be included from another HTTPProxy, +possibly in another namespace.

+
+ingressClassName +
+ +string + +
+(Optional) +

IngressClassName optionally specifies the ingress class to use for this +HTTPProxy. This replaces the deprecated kubernetes.io/ingress.class +annotation. For backwards compatibility, when that annotation is set, it +is given precedence over this field.

+
+

HTTPProxyStatus +

+

+(Appears on: +HTTPProxy) +

+

+

HTTPProxyStatus reports the current state of the HTTPProxy.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+currentStatus +
+ +string + +
+(Optional) +
+description +
+ +string + +
+(Optional) +
+loadBalancer +
+ + +Kubernetes core/v1.LoadBalancerStatus + + +
+(Optional) +

LoadBalancer contains the current status of the load balancer.

+
+conditions +
+ + +[]DetailedCondition + + +
+(Optional) +

Conditions contains information about the current status of the HTTPProxy, +in an upstream-friendly container.

+

Contour will update a single condition, Valid, that is in normal-true polarity. +That is, when currentStatus is valid, the Valid condition will be status: true, +and vice versa.

+

Contour will leave untouched any other Conditions set in this block, +in case some other controller wants to add a Condition.

+

If you are another controller owner and wish to add a condition, you should +namespace your condition with a label, like controller.domain.com/ConditionName.

+
+

HTTPRequestRedirectPolicy +

+

+(Appears on: +Route) +

+

+

HTTPRequestRedirectPolicy defines configuration for redirecting a request.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+scheme +
+ +string + +
+(Optional) +

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.

+
+hostname +
+ +string + +
+(Optional) +

Hostname is the precise hostname to be used in the value of the Location +header in the response. +When empty, the hostname of the request is used. +No wildcards are allowed.

+
+port +
+ +int32 + +
+(Optional) +

Port is the port to be used in the value of the Location +header in the response. +When empty, port (if specified) of the request is used.

+
+statusCode +
+ +int + +
+(Optional) +

StatusCode is the HTTP status code to be used in response.

+
+path +
+ +string + +
+(Optional) +

Path allows for redirection to a different path from the +original on the request. The path must start with a +leading slash.

+

Note: Only one of Path or Prefix can be defined.

+
+prefix +
+ +string + +
+(Optional) +

Prefix defines the value to swap the matched prefix or path with. +The prefix must start with a leading slash.

+

Note: Only one of Path or Prefix can be defined.

+
+

HTTPStatusRange +

+

+(Appears on: +HTTPHealthCheckPolicy) +

+

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+start +
+ +int64 + +
+

The start (inclusive) of a range of HTTP status codes.

+
+end +
+ +int64 + +
+

The end (exclusive) of a range of HTTP status codes.

+
+

HeaderHashOptions +

+

+(Appears on: +RequestHashPolicy) +

+

+

HeaderHashOptions contains options to configure a HTTP request header hash +policy, used in request attribute hash based load balancing.

+

+ + + + + + + + + + + + + +
FieldDescription
+headerName +
+ +string + +
+

HeaderName is the name of the HTTP request header that will be used to +calculate the hash key. If the header specified is not present on a +request, no hash will be produced.

+
+

HeaderMatchCondition +

+

+(Appears on: +MatchCondition, +RequestHeaderValueMatchDescriptor) +

+

+

HeaderMatchCondition specifies how to conditionally match against HTTP +headers. The Name field is required, only one of Present, NotPresent, +Contains, NotContains, Exact, NotExact and Regex can be set. +For negative matching rules only (e.g. NotContains or NotExact) you can set +TreatMissingAsEmpty. +IgnoreCase has no effect for Regex.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Name is the name of the header to match against. Name is required. +Header names are case insensitive.

+
+present +
+ +bool + +
+(Optional) +

Present specifies that condition is true when the named header +is present, regardless of its value. Note that setting Present +to false does not make the condition true if the named header +is absent.

+
+notpresent +
+ +bool + +
+(Optional) +

NotPresent specifies that condition is true when the named header +is not present. Note that setting NotPresent to false does not +make the condition true if the named header is present.

+
+contains +
+ +string + +
+(Optional) +

Contains specifies a substring that must be present in +the header value.

+
+notcontains +
+ +string + +
+(Optional) +

NotContains specifies a substring that must not be present +in the header value.

+
+ignoreCase +
+ +bool + +
+(Optional) +

IgnoreCase specifies that string matching should be case insensitive. +Note that this has no effect on the Regex parameter.

+
+exact +
+ +string + +
+(Optional) +

Exact specifies a string that the header value must be equal to.

+
+notexact +
+ +string + +
+(Optional) +

NoExact specifies a string that the header value must not be +equal to. The condition is true if the header has any other value.

+
+regex +
+ +string + +
+(Optional) +

Regex specifies a regular expression pattern that must match the header +value.

+
+treatMissingAsEmpty +
+ +bool + +
+(Optional) +

TreatMissingAsEmpty specifies if the header match rule specified header +does not exist, this header value will be treated as empty. Defaults to false. +Unlike the underlying Envoy implementation this is only supported for +negative matches (e.g. NotContains, NotExact).

+
+

HeaderValue +

+

+(Appears on: +HeadersPolicy, +LocalRateLimitPolicy) +

+

+

HeaderValue represents a header name/value pair

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Name represents a key of a header

+
+value +
+ +string + +
+

Value represents the value of a header specified by a key

+
+

HeadersPolicy +

+

+(Appears on: +Route, +Service) +

+

+

HeadersPolicy defines how headers are managed during forwarding. +The Host header is treated specially and if set in a HTTP request +will be used as the SNI server name when forwarding over TLS. It is an +error to attempt to set the Host header in a HTTP response.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+set +
+ + +[]HeaderValue + + +
+(Optional) +

Set specifies a list of HTTP header values that will be set in the HTTP header. +If the header does not exist it will be added, otherwise it will be overwritten with the new value.

+
+remove +
+ +[]string + +
+(Optional) +

Remove specifies a list of HTTP header names to remove.

+
+

IPFilterPolicy +

+

+(Appears on: +Route, +VirtualHost) +

+

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+source +
+ + +IPFilterSource + + +
+

Source indicates how to determine the ip address to filter on, and can be +one of two values: +- Remote filters on the ip address of the client, accounting for PROXY and +X-Forwarded-For as needed. +- Peer filters on the ip of the network request, ignoring PROXY and +X-Forwarded-For.

+
+cidr +
+ +string + +
+

CIDR is a CIDR block of ipv4 or ipv6 addresses to filter on. This can also be +a bare IP address (without a mask) to filter on exactly one address.

+
+

IPFilterSource +(string alias)

+

+(Appears on: +IPFilterPolicy) +

+

+

IPFilterSource indicates which IP should be considered for filtering

+

+ + + + + + + + + + + + +
ValueDescription

"Peer"

"Remote"

+

Include +

+

+(Appears on: +HTTPProxySpec) +

+

+

Include describes a set of policies that can be applied to an HTTPProxy in a namespace.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Name of the HTTPProxy

+
+namespace +
+ +string + +
+(Optional) +

Namespace of the HTTPProxy to include. Defaults to the current namespace if not supplied.

+
+conditions +
+ + +[]MatchCondition + + +
+(Optional) +

Conditions are a set of rules that are applied to included HTTPProxies. +In effect, they are added onto the Conditions of included HTTPProxy Route +structs. +When applied, they are merged using AND, with one exception: +There can be only one Prefix MatchCondition per Conditions slice. +More than one Prefix, or contradictory Conditions, will make the +include invalid. Exact and Regex match conditions are not allowed +on includes.

+
+

JWTProvider +

+

+(Appears on: +VirtualHost) +

+

+

JWTProvider defines how to verify JWTs on requests.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Unique name for the provider.

+
+default +
+ +bool + +
+(Optional) +

Whether the provider should apply to all +routes in the HTTPProxy/its includes by +default. At most one provider can be marked +as the default. If no provider is marked +as the default, individual routes must explicitly +identify the provider they require.

+
+issuer +
+ +string + +
+(Optional) +

Issuer that JWTs are required to have in the “iss” field. +If not provided, JWT issuers are not checked.

+
+audiences +
+ +[]string + +
+(Optional) +

Audiences that JWTs are allowed to have in the “aud” field. +If not provided, JWT audiences are not checked.

+
+remoteJWKS +
+ + +RemoteJWKS + + +
+

Remote JWKS to use for verifying JWT signatures.

+
+forwardJWT +
+ +bool + +
+(Optional) +

Whether the JWT should be forwarded to the backend +service after successful verification. By default, +the JWT is not forwarded.

+
+

JWTVerificationPolicy +

+

+(Appears on: +Route) +

+

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+require +
+ +string + +
+(Optional) +

Require names a specific JWT provider (defined in the virtual host) +to require for the route. If specified, this field overrides the +default provider if one exists. If this field is not specified, +the default provider will be required if one exists. At most one of +this field or the “disabled” field can be specified.

+
+disabled +
+ +bool + +
+(Optional) +

Disabled defines whether to disable all JWT verification for this +route. This can be used to opt specific routes out of the default +JWT provider for the HTTPProxy. At most one of this field or the +“require” field can be specified.

+
+

LoadBalancerPolicy +

+

+(Appears on: +Route, +TCPProxy, +ExtensionServiceSpec) +

+

+

LoadBalancerPolicy defines the load balancing policy.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+strategy +
+ +string + +
+

Strategy specifies the policy used to balance requests +across the pool of backend pods. Valid policy names are +Random, RoundRobin, WeightedLeastRequest, Cookie, +and RequestHash. If an unknown strategy name is specified +or no policy is supplied, the default RoundRobin policy +is used.

+
+requestHashPolicies +
+ + +[]RequestHashPolicy + + +
+

RequestHashPolicies contains a list of hash policies to apply when the +RequestHash load balancing strategy is chosen. If an element of the +supplied list of hash policies is invalid, it will be ignored. If the +list of hash policies is empty after validation, the load balancing +strategy will fall back to the default RoundRobin.

+
+

LocalRateLimitPolicy +

+

+(Appears on: +RateLimitPolicy) +

+

+

LocalRateLimitPolicy defines local rate limiting parameters.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+requests +
+ +uint32 + +
+

Requests defines how many requests per unit of time should +be allowed before rate limiting occurs.

+
+unit +
+ +string + +
+

Unit defines the period of time within which requests +over the limit will be rate limited. Valid values are +“second”, “minute” and “hour”.

+
+burst +
+ +uint32 + +
+(Optional) +

Burst defines the number of requests above the requests per +unit that should be allowed within a short period of time.

+
+responseStatusCode +
+ +uint32 + +
+(Optional) +

ResponseStatusCode is the HTTP status code to use for responses +to rate-limited requests. Codes must be in the 400-599 range +(inclusive). If not specified, the Envoy default of 429 (Too +Many Requests) is used.

+
+responseHeadersToAdd +
+ + +[]HeaderValue + + +
+(Optional) +

ResponseHeadersToAdd is an optional list of response headers to +set when a request is rate-limited.

+
+

MatchCondition +

+

+(Appears on: +Include, +Route) +

+

+

MatchCondition are a general holder for matching rules for HTTPProxies. +One of Prefix, Exact, Regex, Header or QueryParameter must be provided.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+prefix +
+ +string + +
+(Optional) +

Prefix defines a prefix match for a request.

+
+exact +
+ +string + +
+(Optional) +

Exact defines a exact match for a request. +This field is not allowed in include match conditions.

+
+regex +
+ +string + +
+(Optional) +

Regex defines a regex match for a request. +This field is not allowed in include match conditions.

+
+header +
+ + +HeaderMatchCondition + + +
+(Optional) +

Header specifies the header condition to match.

+
+queryParameter +
+ + +QueryParameterMatchCondition + + +
+(Optional) +

QueryParameter specifies the query parameter condition to match.

+
+

Namespace +(string alias)

+

+(Appears on: +ContourSettings) +

+

+

Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.

+

This validation is based off of the corresponding Kubernetes validation: +https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187

+

This is used for Namespace name validation here: +https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63

+

Valid values include:

+ +

Invalid values include:

+ +

+

PathRewritePolicy +

+

+(Appears on: +Route) +

+

+

PathRewritePolicy specifies how a request URL path should be +rewritten. This rewriting takes place after a request is routed +and has no subsequent effects on the proxy’s routing decision. +No HTTP headers or body content is rewritten.

+

Exactly one field in this struct may be specified.

+

+ + + + + + + + + + + + + +
FieldDescription
+replacePrefix +
+ + +[]ReplacePrefix + + +
+(Optional) +

ReplacePrefix describes how the path prefix should be replaced.

+
+

QueryParameterHashOptions +

+

+(Appears on: +RequestHashPolicy) +

+

+

QueryParameterHashOptions contains options to configure a query parameter based hash +policy, used in request attribute hash based load balancing.

+

+ + + + + + + + + + + + + +
FieldDescription
+parameterName +
+ +string + +
+

ParameterName is the name of the HTTP request query parameter that will be used to +calculate the hash key. If the query parameter specified is not present on a +request, no hash will be produced.

+
+

QueryParameterMatchCondition +

+

+(Appears on: +MatchCondition) +

+

+

QueryParameterMatchCondition specifies how to conditionally match against HTTP +query parameters. The Name field is required, only one of Exact, Prefix, +Suffix, Regex, Contains and Present can be set. IgnoreCase has no effect +for Regex.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Name is the name of the query parameter to match against. Name is required. +Query parameter names are case insensitive.

+
+exact +
+ +string + +
+(Optional) +

Exact specifies a string that the query parameter value must be equal to.

+
+prefix +
+ +string + +
+(Optional) +

Prefix defines a prefix match for the query parameter value.

+
+suffix +
+ +string + +
+(Optional) +

Suffix defines a suffix match for a query parameter value.

+
+regex +
+ +string + +
+(Optional) +

Regex specifies a regular expression pattern that must match the query +parameter value.

+
+contains +
+ +string + +
+(Optional) +

Contains specifies a substring that must be present in +the query parameter value.

+
+ignoreCase +
+ +bool + +
+(Optional) +

IgnoreCase specifies that string matching should be case insensitive. +Note that this has no effect on the Regex parameter.

+
+present +
+ +bool + +
+(Optional) +

Present specifies that condition is true when the named query parameter +is present, regardless of its value. Note that setting Present +to false does not make the condition true if the named query parameter +is absent.

+
+

RateLimitDescriptor +

+

+(Appears on: +GlobalRateLimitPolicy) +

+

+

RateLimitDescriptor defines a list of key-value pair generators.

+

+ + + + + + + + + + + + + +
FieldDescription
+entries +
+ + +[]RateLimitDescriptorEntry + + +
+

Entries is the list of key-value pair generators.

+
+

RateLimitDescriptorEntry +

+

+(Appears on: +RateLimitDescriptor) +

+

+

RateLimitDescriptorEntry is a key-value pair generator. Exactly +one field on this struct must be non-nil.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+genericKey +
+ + +GenericKeyDescriptor + + +
+(Optional) +

GenericKey defines a descriptor entry with a static key and value.

+
+requestHeader +
+ + +RequestHeaderDescriptor + + +
+(Optional) +

RequestHeader defines a descriptor entry that’s populated only if +a given header is present on the request. The descriptor key is static, +and the descriptor value is equal to the value of the header.

+
+requestHeaderValueMatch +
+ + +RequestHeaderValueMatchDescriptor + + +
+(Optional) +

RequestHeaderValueMatch defines a descriptor entry that’s populated +if the request’s headers match a set of 1+ match criteria. The +descriptor key is “header_match”, and the descriptor value is static.

+
+remoteAddress +
+ + +RemoteAddressDescriptor + + +
+(Optional) +

RemoteAddress defines a descriptor entry with a key of “remote_address” +and a value equal to the client’s IP address (from x-forwarded-for).

+
+

RateLimitPolicy +

+

+(Appears on: +Route, +VirtualHost) +

+

+

RateLimitPolicy defines rate limiting parameters.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+local +
+ + +LocalRateLimitPolicy + + +
+(Optional) +

Local defines local rate limiting parameters, i.e. parameters +for rate limiting that occurs within each Envoy pod as requests +are handled.

+
+global +
+ + +GlobalRateLimitPolicy + + +
+(Optional) +

Global defines global rate limiting parameters, i.e. parameters +defining descriptors that are sent to an external rate limit +service (RLS) for a rate limit decision on each request.

+
+

RedirectResponseCode +(uint32 alias)

+

+(Appears on: +HTTPInternalRedirectPolicy) +

+

+

RedirectResponseCode is a uint32 type alias with validation to ensure that the value is valid.

+

+

RemoteAddressDescriptor +

+

+(Appears on: +RateLimitDescriptorEntry) +

+

+

RemoteAddressDescriptor defines a descriptor entry with a key of +“remote_address” and a value equal to the client’s IP address +(from x-forwarded-for).

+

+

RemoteJWKS +

+

+(Appears on: +JWTProvider) +

+

+

RemoteJWKS defines how to fetch a JWKS from an HTTP endpoint.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+uri +
+ +string + +
+

The URI for the JWKS.

+
+validation +
+ + +UpstreamValidation + + +
+(Optional) +

UpstreamValidation defines how to verify the JWKS’s TLS certificate.

+
+timeout +
+ +string + +
+(Optional) +

How long to wait for a response from the URI. +If not specified, a default of 1s applies.

+
+cacheDuration +
+ +string + +
+(Optional) +

How long to cache the JWKS locally. If not specified, +Envoy’s default of 5m applies.

+
+dnsLookupFamily +
+ +string + +
+(Optional) +

The DNS IP address resolution policy for the JWKS URI. +When configured as “v4”, the DNS resolver will only perform a lookup +for addresses in the IPv4 family. If “v6” is configured, the DNS resolver +will only perform a lookup for addresses in the IPv6 family. +If “all” is configured, the DNS resolver +will perform a lookup for addresses in both the IPv4 and IPv6 family. +If “auto” is configured, the DNS resolver will first perform a lookup +for addresses in the IPv6 family and fallback to a lookup for addresses +in the IPv4 family. If not specified, the Contour-wide setting defined +in the config file or ContourConfiguration applies (defaults to “auto”).

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto.html#envoy-v3-api-enum-config-cluster-v3-cluster-dnslookupfamily +for more information.

+
+

ReplacePrefix +

+

+(Appears on: +PathRewritePolicy) +

+

+

ReplacePrefix describes a path prefix replacement.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+prefix +
+ +string + +
+(Optional) +

Prefix specifies the URL path prefix to be replaced.

+

If Prefix is specified, it must exactly match the MatchCondition +prefix that is rendered by the chain of including HTTPProxies +and only that path prefix will be replaced by Replacement. +This allows HTTPProxies that are included through multiple +roots to only replace specific path prefixes, leaving others +unmodified.

+

If Prefix is not specified, all routing prefixes rendered +by the include chain will be replaced.

+
+replacement +
+ +string + +
+

Replacement is the string that the routing path prefix +will be replaced with. This must not be empty.

+
+

RequestHashPolicy +

+

+(Appears on: +LoadBalancerPolicy) +

+

+

RequestHashPolicy contains configuration for an individual hash policy +on a request attribute.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+terminal +
+ +bool + +
+

Terminal is a flag that allows for short-circuiting computing of a hash +for a given request. If set to true, and the request attribute specified +in the attribute hash options is present, no further hash policies will +be used to calculate a hash for the request.

+
+headerHashOptions +
+ + +HeaderHashOptions + + +
+(Optional) +

HeaderHashOptions should be set when request header hash based load +balancing is desired. It must be the only hash option field set, +otherwise this request hash policy object will be ignored.

+
+queryParameterHashOptions +
+ + +QueryParameterHashOptions + + +
+(Optional) +

QueryParameterHashOptions should be set when request query parameter hash based load +balancing is desired. It must be the only hash option field set, +otherwise this request hash policy object will be ignored.

+
+hashSourceIP +
+ +bool + +
+(Optional) +

HashSourceIP should be set to true when request source IP hash based +load balancing is desired. It must be the only hash option field set, +otherwise this request hash policy object will be ignored.

+
+

RequestHeaderDescriptor +

+

+(Appears on: +RateLimitDescriptorEntry) +

+

+

RequestHeaderDescriptor defines a descriptor entry that’s populated only +if a given header is present on the request. The value of the descriptor +entry is equal to the value of the header (if present).

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+headerName +
+ +string + +
+

HeaderName defines the name of the header to look for on the request.

+
+descriptorKey +
+ +string + +
+

DescriptorKey defines the key to use on the descriptor entry.

+
+

RequestHeaderValueMatchDescriptor +

+

+(Appears on: +RateLimitDescriptorEntry) +

+

+

RequestHeaderValueMatchDescriptor defines a descriptor entry that’s populated +if the request’s headers match a set of 1+ match criteria. The descriptor key +is “header_match”, and the descriptor value is statically defined.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+headers +
+ + +[]HeaderMatchCondition + + +
+

Headers is a list of 1+ match criteria to apply against the request +to determine whether to populate the descriptor entry or not.

+
+expectMatch +
+ +bool + +
+

ExpectMatch defines whether the request must positively match the match +criteria in order to generate a descriptor entry (i.e. true), or not +match the match criteria in order to generate a descriptor entry (i.e. false). +The default is true.

+
+value +
+ +string + +
+

Value defines the value of the descriptor entry.

+
+

RetryOn +(string alias)

+

+(Appears on: +RetryPolicy) +

+

+

RetryOn is a string type alias with validation to ensure that the value is valid.

+

+

RetryPolicy +

+

+(Appears on: +Route) +

+

+

RetryPolicy defines the attributes associated with retrying policy.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+count +
+ +int64 + +
+(Optional) +

NumRetries is maximum allowed number of retries. +If set to -1, then retries are disabled. +If set to 0 or not supplied, the value is set +to the Envoy default of 1.

+
+perTryTimeout +
+ +string + +
+(Optional) +

PerTryTimeout specifies the timeout per retry attempt. +Ignored if NumRetries is not supplied.

+
+retryOn +
+ + +[]RetryOn + + +
+(Optional) +

RetryOn specifies the conditions on which to retry a request.

+

Supported HTTP conditions:

+
    +
  • 5xx
  • +
  • gateway-error
  • +
  • reset
  • +
  • connect-failure
  • +
  • retriable-4xx
  • +
  • refused-stream
  • +
  • retriable-status-codes
  • +
  • retriable-headers
  • +
+

Supported gRPC conditions:

+
    +
  • cancelled
  • +
  • deadline-exceeded
  • +
  • internal
  • +
  • resource-exhausted
  • +
  • unavailable
  • +
+
+retriableStatusCodes +
+ +[]uint32 + +
+(Optional) +

RetriableStatusCodes specifies the HTTP status codes that should be retried.

+

This field is only respected when you include retriable-status-codes in the RetryOn field.

+
+

Route +

+

+(Appears on: +HTTPProxySpec) +

+

+

Route contains the set of routes for a virtual host.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+conditions +
+ + +[]MatchCondition + + +
+(Optional) +

Conditions are a set of rules that are applied to a Route. +When applied, they are merged using AND, with one exception: +There can be only one Prefix, Exact or Regex MatchCondition +per Conditions slice. More than one of these condition types, +or contradictory Conditions, will make the route invalid.

+
+services +
+ + +[]Service + + +
+(Optional) +

Services are the services to proxy traffic.

+
+enableWebsockets +
+ +bool + +
+(Optional) +

Enables websocket support for the route.

+
+permitInsecure +
+ +bool + +
+(Optional) +

Allow this path to respond to insecure requests over HTTP which are normally +not permitted when a virtualhost.tls block is present.

+
+authPolicy +
+ + +AuthorizationPolicy + + +
+(Optional) +

AuthPolicy updates the authorization policy that was set +on the root HTTPProxy object for client requests that +match this route.

+
+timeoutPolicy +
+ + +TimeoutPolicy + + +
+(Optional) +

The timeout policy for this route.

+
+retryPolicy +
+ + +RetryPolicy + + +
+(Optional) +

The retry policy for this route.

+
+healthCheckPolicy +
+ + +HTTPHealthCheckPolicy + + +
+(Optional) +

The health check policy for this route.

+
+loadBalancerPolicy +
+ + +LoadBalancerPolicy + + +
+(Optional) +

The load balancing policy for this route.

+
+pathRewritePolicy +
+ + +PathRewritePolicy + + +
+(Optional) +

The policy for rewriting the path of the request URL +after the request has been routed to a Service.

+
+requestHeadersPolicy +
+ + +HeadersPolicy + + +
+(Optional) +

The policy for managing request headers during proxying.

+

You may dynamically rewrite the Host header to be forwarded +upstream to the content of a request header using +the below format “%REQ(X-Header-Name)%”. If the value of the header +is empty, it is ignored.

+

*NOTE: Pay attention to the potential security implications of using this option. +Provided header must come from trusted source.

+

**NOTE: The header rewrite is only done while forwarding and has no bearing +on the routing decision.

+
+responseHeadersPolicy +
+ + +HeadersPolicy + + +
+(Optional) +

The policy for managing response headers during proxying. +Rewriting the ‘Host’ header is not supported.

+
+cookieRewritePolicies +
+ + +[]CookieRewritePolicy + + +
+(Optional) +

The policies for rewriting Set-Cookie header attributes. Note that +rewritten cookie names must be unique in this list. Order rewrite +policies are specified in does not matter.

+
+rateLimitPolicy +
+ + +RateLimitPolicy + + +
+(Optional) +

The policy for rate limiting on the route.

+
+requestRedirectPolicy +
+ + +HTTPRequestRedirectPolicy + + +
+(Optional) +

RequestRedirectPolicy defines an HTTP redirection.

+
+directResponsePolicy +
+ + +HTTPDirectResponsePolicy + + +
+(Optional) +

DirectResponsePolicy returns an arbitrary HTTP response directly.

+
+internalRedirectPolicy +
+ + +HTTPInternalRedirectPolicy + + +
+(Optional) +

The policy to define when to handle redirects responses internally.

+
+jwtVerificationPolicy +
+ + +JWTVerificationPolicy + + +
+(Optional) +

The policy for verifying JWTs for requests to this route.

+
+ipAllowPolicy +
+ + +[]IPFilterPolicy + + +
+

IPAllowFilterPolicy is a list of ipv4/6 filter rules for which matching +requests should be allowed. All other requests will be denied. +Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. +The rules defined here override any rules set on the root HTTPProxy.

+
+ipDenyPolicy +
+ + +[]IPFilterPolicy + + +
+

IPDenyFilterPolicy is a list of ipv4/6 filter rules for which matching +requests should be denied. All other requests will be allowed. +Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. +The rules defined here override any rules set on the root HTTPProxy.

+
+

Service +

+

+(Appears on: +Route, +TCPProxy) +

+

+

Service defines an Kubernetes Service to proxy traffic.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Name is the name of Kubernetes service to proxy traffic. +Names defined here will be used to look up corresponding endpoints which contain the ips to route.

+
+port +
+ +int + +
+

Port (defined as Integer) to proxy traffic to since a service can have multiple defined.

+
+healthPort +
+ +int + +
+(Optional) +

HealthPort is the port for this service healthcheck. +If not specified, Port is used for service healthchecks.

+
+protocol +
+ +string + +
+(Optional) +

Protocol may be used to specify (or override) the protocol used to reach this Service. +Values may be tls, h2, h2c. If omitted, protocol-selection falls back on Service annotations.

+
+weight +
+ +int64 + +
+(Optional) +

Weight defines percentage of traffic to balance traffic

+
+validation +
+ + +UpstreamValidation + + +
+(Optional) +

UpstreamValidation defines how to verify the backend service’s certificate

+
+mirror +
+ +bool + +
+

If Mirror is true the Service will receive a read only mirror of the traffic for this route. +If Mirror is true, then fractional mirroring can be enabled by optionally setting the Weight +field. Legal values for Weight are 1-100. Omitting the Weight field will result in 100% mirroring. +NOTE: Setting Weight explicitly to 0 will unexpectedly result in 100% traffic mirroring. This +occurs since we cannot distinguish omitted fields from those explicitly set to their default +values

+
+requestHeadersPolicy +
+ + +HeadersPolicy + + +
+(Optional) +

The policy for managing request headers during proxying.

+
+responseHeadersPolicy +
+ + +HeadersPolicy + + +
+(Optional) +

The policy for managing response headers during proxying. +Rewriting the ‘Host’ header is not supported.

+
+cookieRewritePolicies +
+ + +[]CookieRewritePolicy + + +
+(Optional) +

The policies for rewriting Set-Cookie header attributes.

+
+slowStartPolicy +
+ + +SlowStartPolicy + + +
+(Optional) +

Slow start will gradually increase amount of traffic to a newly added endpoint.

+
+

SlowStartPolicy +

+

+(Appears on: +Service) +

+

+

SlowStartPolicy will gradually increase amount of traffic to a newly added endpoint. +It can be used only with RoundRobin and WeightedLeastRequest load balancing strategies.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+window +
+ +string + +
+

The duration of slow start window. +Duration is expressed in the Go Duration format. +Valid time units are “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”.

+
+aggression +
+ +string + +
+(Optional) +

The speed of traffic increase over the slow start window. +Defaults to 1.0, so that endpoint would get linearly increasing amount of traffic. +When increasing the value for this parameter, the speed of traffic ramp-up increases non-linearly. +The value of aggression parameter should be greater than 0.0.

+

More info: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/slow_start

+
+minWeightPercent +
+ +uint32 + +
+(Optional) +

The minimum or starting percentage of traffic to send to new endpoints. +A non-zero value helps avoid a too small initial weight, which may cause endpoints in slow start mode to receive no traffic in the beginning of the slow start window. +If not specified, the default is 10%.

+
+

SubCondition +

+

+(Appears on: +DetailedCondition) +

+

+

SubCondition is a Condition-like type intended for use as a subcondition inside a DetailedCondition.

+

It contains a subset of the Condition fields.

+

It is intended for warnings and errors, so type names should use abnormal-true polarity, +that is, they should be of the form “ErrorPresent: true”.

+

The expected lifecycle for these errors is that they should only be present when the error or warning is, +and should be removed when they are not relevant.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+type +
+ +string + +
+

Type of condition in CamelCase or in foo.example.com/CamelCase.

+

This must be in abnormal-true polarity, that is, ErrorFound or controller.io/ErrorFound.

+

The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)

+
+status +
+ + +Kubernetes meta/v1.ConditionStatus + + +
+

Status of the condition, one of True, False, Unknown.

+
+reason +
+ +string + +
+

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.

+
+message +
+ +string + +
+

Message is a human readable message indicating details about the transition.

+

This may be an empty string.

+
+

TCPHealthCheckPolicy +

+

+(Appears on: +TCPProxy) +

+

+

TCPHealthCheckPolicy defines health checks on the upstream service.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+intervalSeconds +
+ +int64 + +
+(Optional) +

The interval (seconds) between health checks

+
+timeoutSeconds +
+ +int64 + +
+(Optional) +

The time to wait (seconds) for a health check response

+
+unhealthyThresholdCount +
+ +uint32 + +
+(Optional) +

The number of unhealthy health checks required before a host is marked unhealthy

+
+healthyThresholdCount +
+ +uint32 + +
+(Optional) +

The number of healthy health checks required before a host is marked healthy

+
+

TCPProxy +

+

+(Appears on: +HTTPProxySpec) +

+

+

TCPProxy contains the set of services to proxy TCP connections.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+loadBalancerPolicy +
+ + +LoadBalancerPolicy + + +
+(Optional) +

The load balancing policy for the backend services. Note that the +Cookie and RequestHash load balancing strategies cannot be used +here.

+
+services +
+ + +[]Service + + +
+(Optional) +

Services are the services to proxy traffic

+
+include +
+ + +TCPProxyInclude + + +
+(Optional) +

Include specifies that this tcpproxy should be delegated to another HTTPProxy.

+
+includes +
+ + +TCPProxyInclude + + +
+(Optional) +

IncludesDeprecated allow for specific routing configuration to be appended to another HTTPProxy in another namespace.

+

Exists due to a mistake when developing HTTPProxy and the field was marked plural +when it should have been singular. This field should stay to not break backwards compatibility to v1 users.

+
+healthCheckPolicy +
+ + +TCPHealthCheckPolicy + + +
+(Optional) +

The health check policy for this tcp proxy

+
+

TCPProxyInclude +

+

+(Appears on: +TCPProxy) +

+

+

TCPProxyInclude describes a target HTTPProxy document which contains the TCPProxy details.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Name of the child HTTPProxy

+
+namespace +
+ +string + +
+(Optional) +

Namespace of the HTTPProxy to include. Defaults to the current namespace if not supplied.

+
+

TLS +

+

+(Appears on: +VirtualHost) +

+

+

TLS describes tls properties. The SNI names that will be matched on +are described in the HTTPProxy’s Spec.VirtualHost.Fqdn field.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+secretName +
+ +string + +
+

SecretName is the name of a TLS secret. +Either SecretName or Passthrough must be specified, but not both. +If specified, the named secret must contain a matching certificate +for the virtual host’s FQDN. +The name can be optionally prefixed with namespace “namespace/name”. +When cross-namespace reference is used, TLSCertificateDelegation resource must exist in the namespace to grant access to the secret.

+
+minimumProtocolVersion +
+ +string + +
+(Optional) +

MinimumProtocolVersion is the minimum TLS version this vhost should +negotiate. Valid options are 1.2 (default) and 1.3. Any other value +defaults to TLS 1.2.

+
+maximumProtocolVersion +
+ +string + +
+(Optional) +

MaximumProtocolVersion is the maximum TLS version this vhost should +negotiate. Valid options are 1.2 and 1.3 (default). Any other value +defaults to TLS 1.3.

+
+passthrough +
+ +bool + +
+(Optional) +

Passthrough defines whether the encrypted TLS handshake will be +passed through to the backing cluster. Either Passthrough or +SecretName must be specified, but not both.

+
+clientValidation +
+ + +DownstreamValidation + + +
+(Optional) +

ClientValidation defines how to verify the client certificate +when an external client establishes a TLS connection to Envoy.

+

This setting:

+
    +
  1. Enables TLS client certificate validation.
  2. +
  3. Specifies how the client certificate will be validated (i.e. +validation required or skipped).
  4. +
+

Note: Setting client certificate validation to be skipped should +be only used in conjunction with an external authorization server that +performs client validation as Contour will ensure client certificates +are passed along.

+
+enableFallbackCertificate +
+ +bool + +
+

EnableFallbackCertificate defines if the vhost should allow a default certificate to +be applied which handles all requests which don’t match the SNI defined in this vhost.

+
+

TLSCertificateDelegationSpec +

+

+(Appears on: +TLSCertificateDelegation) +

+

+

TLSCertificateDelegationSpec defines the spec of the CRD

+

+ + + + + + + + + + + + + +
FieldDescription
+delegations +
+ + +[]CertificateDelegation + + +
+
+

TLSCertificateDelegationStatus +

+

+(Appears on: +TLSCertificateDelegation) +

+

+

TLSCertificateDelegationStatus allows for the status of the delegation +to be presented to the user.

+

+ + + + + + + + + + + + + +
FieldDescription
+conditions +
+ + +[]DetailedCondition + + +
+(Optional) +

Conditions contains information about the current status of the HTTPProxy, +in an upstream-friendly container.

+

Contour will update a single condition, Valid, that is in normal-true polarity. +That is, when currentStatus is valid, the Valid condition will be status: true, +and vice versa.

+

Contour will leave untouched any other Conditions set in this block, +in case some other controller wants to add a Condition.

+

If you are another controller owner and wish to add a condition, you should +namespace your condition with a label, like controller.domain.com\ConditionName.

+
+

TimeoutPolicy +

+

+(Appears on: +Route, +ExtensionServiceSpec) +

+

+

TimeoutPolicy configures timeouts that are used for handling network requests.

+

TimeoutPolicy durations are expressed in the Go Duration format. +Valid time units are “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”. +The string “infinity” is also a valid input and specifies no timeout. +A value of “0s” will be treated as if the field were not set, i.e. by using Envoy’s default behavior.

+

Example input values: “300ms”, “5s”, “1m”.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+response +
+ +string + +
+(Optional) +

Timeout for receiving a response from the server after processing a request from client. +If not supplied, Envoy’s default value of 15s applies.

+
+idle +
+ +string + +
+(Optional) +

Timeout for how long the proxy should wait while there is no activity during single request/response (for HTTP/1.1) or stream (for HTTP/2). +Timeout will not trigger while HTTP/1.1 connection is idle between two consecutive requests. +If not specified, there is no per-route idle timeout, though a connection manager-wide +stream_idle_timeout default of 5m still applies.

+
+idleConnection +
+ +string + +
+(Optional) +

Timeout for how long connection from the proxy to the upstream service is kept when there are no active requests. +If not supplied, Envoy’s default value of 1h applies.

+
+

UpstreamValidation +

+

+(Appears on: +RemoteJWKS, +Service, +ExtensionServiceSpec) +

+

+

UpstreamValidation defines how to verify the backend service’s certificate

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+caSecret +
+ +string + +
+

Name or namespaced name of the Kubernetes secret used to validate the certificate presented by the backend. +The secret must contain key named ca.crt. +The name can be optionally prefixed with namespace “namespace/name”. +When cross-namespace reference is used, TLSCertificateDelegation resource must exist in the namespace to grant access to the secret. +Max length should be the actual max possible length of a namespaced name (63 + 253 + 1 = 317)

+
+subjectName +
+ +string + +
+

Key which is expected to be present in the ‘subjectAltName’ of the presented certificate. +Deprecated: migrate to using the plural field subjectNames.

+
+subjectNames +
+ +[]string + +
+(Optional) +

List of keys, of which at least one is expected to be present in the ‘subjectAltName of the +presented certificate.

+
+

VirtualHost +

+

+(Appears on: +HTTPProxySpec) +

+

+

VirtualHost appears at most once. If it is present, the object is considered +to be a “root”.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+fqdn +
+ +string + +
+

The fully qualified domain name of the root of the ingress tree +all leaves of the DAG rooted at this object relate to the fqdn.

+
+tls +
+ + +TLS + + +
+(Optional) +

If present the fields describes TLS properties of the virtual +host. The SNI names that will be matched on are described in fqdn, +the tls.secretName secret must contain a certificate that itself +contains a name that matches the FQDN.

+
+authorization +
+ + +AuthorizationServer + + +
+(Optional) +

This field configures an extension service to perform +authorization for this virtual host. Authorization can +only be configured on virtual hosts that have TLS enabled. +If the TLS configuration requires client certificate +validation, the client certificate is always included in the +authentication check request.

+
+corsPolicy +
+ + +CORSPolicy + + +
+(Optional) +

Specifies the cross-origin policy to apply to the VirtualHost.

+
+rateLimitPolicy +
+ + +RateLimitPolicy + + +
+(Optional) +

The policy for rate limiting on the virtual host.

+
+jwtProviders +
+ + +[]JWTProvider + + +
+(Optional) +

Providers to use for verifying JSON Web Tokens (JWTs) on the virtual host.

+
+ipAllowPolicy +
+ + +[]IPFilterPolicy + + +
+

IPAllowFilterPolicy is a list of ipv4/6 filter rules for which matching +requests should be allowed. All other requests will be denied. +Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. +The rules defined here may be overridden in a Route.

+
+ipDenyPolicy +
+ + +[]IPFilterPolicy + + +
+

IPDenyFilterPolicy is a list of ipv4/6 filter rules for which matching +requests should be denied. All other requests will be allowed. +Only one of IPAllowFilterPolicy and IPDenyFilterPolicy can be defined. +The rules defined here may be overridden in a Route.

+
+
+

projectcontour.io/v1alpha1

+

+

Package v1alpha1 contains API Schema definitions for the projectcontour.io v1alpha1 API group

+

+Resource Types: + +

ContourConfiguration +

+

+

ContourConfiguration is the schema for a Contour instance.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +projectcontour.io/v1alpha1 + +
+kind
+string +
ContourConfiguration
+metadata +
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec +
+ + +ContourConfigurationSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+xdsServer +
+ + +XDSServerConfig + + +
+(Optional) +

XDSServer contains parameters for the xDS server.

+
+ingress +
+ + +IngressConfig + + +
+(Optional) +

Ingress contains parameters for ingress options.

+
+debug +
+ + +DebugConfig + + +
+(Optional) +

Debug contains parameters to enable debug logging +and debug interfaces inside Contour.

+
+health +
+ + +HealthConfig + + +
+(Optional) +

Health defines the endpoints Contour uses to serve health checks.

+

Contour’s default is { address: “0.0.0.0”, port: 8000 }.

+
+envoy +
+ + +EnvoyConfig + + +
+(Optional) +

Envoy contains parameters for Envoy as well +as how to optionally configure a managed Envoy fleet.

+
+gateway +
+ + +GatewayConfig + + +
+(Optional) +

Gateway contains parameters for the gateway-api Gateway that Contour +is configured to serve traffic.

+
+httpproxy +
+ + +HTTPProxyConfig + + +
+(Optional) +

HTTPProxy defines parameters on HTTPProxy.

+
+enableExternalNameService +
+ +bool + +
+(Optional) +

EnableExternalNameService allows processing of ExternalNameServices

+

Contour’s default is false for security reasons.

+
+globalExtAuth +
+ + +AuthorizationServer + + +
+(Optional) +

GlobalExternalAuthorization allows envoys external authorization filter +to be enabled for all virtual hosts.

+
+rateLimitService +
+ + +RateLimitServiceConfig + + +
+(Optional) +

RateLimitService optionally holds properties of the Rate Limit Service +to be used for global rate limiting.

+
+policy +
+ + +PolicyConfig + + +
+(Optional) +

Policy specifies default policy applied if not overridden by the user

+
+metrics +
+ + +MetricsConfig + + +
+(Optional) +

Metrics defines the endpoint Contour uses to serve metrics.

+

Contour’s default is { address: “0.0.0.0”, port: 8000 }.

+
+tracing +
+ + +TracingConfig + + +
+

Tracing defines properties for exporting trace data to OpenTelemetry.

+
+featureFlags +
+ + +FeatureFlags + + +
+

FeatureFlags defines toggle to enable new contour features. +Available toggles are: +useEndpointSlices - Configures contour to fetch endpoint data +from k8s endpoint slices. defaults to true, +If false then reads endpoint data from the k8s endpoints.

+
+
+status +
+ + +ContourConfigurationStatus + + +
+(Optional) +
+

ContourDeployment +

+

+

ContourDeployment is the schema for a Contour Deployment.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +projectcontour.io/v1alpha1 + +
+kind
+string +
ContourDeployment
+metadata +
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec +
+ + +ContourDeploymentSpec + + +
+
+
+ + + + + + + + + + + + + + + + + +
+contour +
+ + +ContourSettings + + +
+(Optional) +

Contour specifies deployment-time settings for the Contour +part of the installation, i.e. the xDS server/control plane +and associated resources, including things like replica count +for the Deployment, and node placement constraints for the pods.

+
+envoy +
+ + +EnvoySettings + + +
+(Optional) +

Envoy specifies deployment-time settings for the Envoy +part of the installation, i.e. the xDS client/data plane +and associated resources, including things like the workload +type to use (DaemonSet or Deployment), node placement constraints +for the pods, and various options for the Envoy service.

+
+runtimeSettings +
+ + +ContourConfigurationSpec + + +
+(Optional) +

RuntimeSettings is a ContourConfiguration spec to be used when +provisioning a Contour instance that will influence aspects of +the Contour instance’s runtime behavior.

+
+resourceLabels +
+ +map[string]string + +
+(Optional) +

ResourceLabels is a set of labels to add to the provisioned Contour resources.

+

Deprecated: use Gateway.Spec.Infrastructure.Labels instead. This field will be +removed in a future release.

+
+
+status +
+ + +ContourDeploymentStatus + + +
+
+

ExtensionService +

+

+

ExtensionService is the schema for the Contour extension services API. +An ExtensionService resource binds a network service to the Contour +API so that Contour API features can be implemented by collaborating +components.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+ +projectcontour.io/v1alpha1 + +
+kind
+string +
ExtensionService
+metadata +
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec +
+ + +ExtensionServiceSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+services +
+ + +[]ExtensionServiceTarget + + +
+

Services specifies the set of Kubernetes Service resources that +receive GRPC extension API requests. +If no weights are specified for any of the entries in +this array, traffic will be spread evenly across all the +services. +Otherwise, traffic is balanced proportionally to the +Weight field in each entry.

+
+validation +
+ + +UpstreamValidation + + +
+(Optional) +

UpstreamValidation defines how to verify the backend service’s certificate

+
+protocol +
+ +string + +
+(Optional) +

Protocol may be used to specify (or override) the protocol used to reach this Service. +Values may be h2 or h2c. If omitted, protocol-selection falls back on Service annotations.

+
+loadBalancerPolicy +
+ + +LoadBalancerPolicy + + +
+(Optional) +

The policy for load balancing GRPC service requests. Note that the +Cookie and RequestHash load balancing strategies cannot be used +here.

+
+timeoutPolicy +
+ + +TimeoutPolicy + + +
+(Optional) +

The timeout policy for requests to the services.

+
+protocolVersion +
+ + +ExtensionProtocolVersion + + +
+(Optional) +

This field sets the version of the GRPC protocol that Envoy uses to +send requests to the extension service. Since Contour always uses the +v3 Envoy API, this is currently fixed at “v3”. However, other +protocol options will be available in future.

+
+
+status +
+ + +ExtensionServiceStatus + + +
+
+

AccessLogFormatString +(string alias)

+

+

+

AccessLogJSONFields +([]string alias)

+

+(Appears on: +EnvoyLogging) +

+

+

+

AccessLogLevel +(string alias)

+

+(Appears on: +EnvoyLogging) +

+

+

+ + + + + + + + + + + + + + + + +
ValueDescription

"critical"

Log only requests that result in an server error (i.e. 500+) response code.

+

"disabled"

Disable the access log.

+

"error"

Log only requests that result in a non-success (i.e. 300+) response code

+

"info"

Log all requests. This is the default.

+
+

AccessLogType +(string alias)

+

+(Appears on: +EnvoyLogging) +

+

+

AccessLogType is the name of a supported access logging mechanism.

+

+ + + + + + + + + + + + + + +
ValueDescription

"envoy"

DefaultAccessLogType is the default access log format.

+

"envoy"

Set the Envoy access logging to Envoy’s standard format. +Can be customized using accessLogFormatString.

+

"json"

Set the Envoy access logging to a JSON format. +Can be customized using jsonFields.

+
+

ClusterDNSFamilyType +(string alias)

+

+(Appears on: +ClusterParameters) +

+

+

ClusterDNSFamilyType is the Ip family to use for resolving DNS +names in an Envoy cluster config.

+

+ + + + + + + + + + + + + + + + +
ValueDescription

"all"

DNS lookups will attempt both v4 and v6 queries.

+

"auto"

DNS lookups will do a v6 lookup first, followed by a v4 if that fails.

+

"v4"

DNS lookups will only attempt v4 queries.

+

"v6"

DNS lookups will only attempt v6 queries.

+
+

ClusterParameters +

+

+(Appears on: +EnvoyConfig) +

+

+

ClusterParameters holds various configurable cluster values.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+dnsLookupFamily +
+ + +ClusterDNSFamilyType + + +
+(Optional) +

DNSLookupFamily defines how external names are looked up +When configured as V4, the DNS resolver will only perform a lookup +for addresses in the IPv4 family. If V6 is configured, the DNS resolver +will only perform a lookup for addresses in the IPv6 family. +If AUTO is configured, the DNS resolver will first perform a lookup +for addresses in the IPv6 family and fallback to a lookup for addresses +in the IPv4 family. If ALL is specified, the DNS resolver will perform a lookup for +both IPv4 and IPv6 families, and return all resolved addresses. +When this is used, Happy Eyeballs will be enabled for upstream connections. +Refer to Happy Eyeballs Support for more information. +Note: This only applies to externalName clusters.

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto.html#envoy-v3-api-enum-config-cluster-v3-cluster-dnslookupfamily +for more information.

+

Values: auto (default), v4, v6, all.

+

Other values will produce an error.

+
+maxRequestsPerConnection +
+ +uint32 + +
+(Optional) +

Defines the maximum requests for upstream connections. If not specified, there is no limit. +see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-msg-config-core-v3-httpprotocoloptions +for more information.

+
+per-connection-buffer-limit-bytes +
+ +uint32 + +
+(Optional) +

Defines the soft limit on size of the cluster’s new connection read and write buffers in bytes. +If unspecified, an implementation defined default is applied (1MiB). +see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-field-config-cluster-v3-cluster-per-connection-buffer-limit-bytes +for more information.

+
+circuitBreakers +
+ + +GlobalCircuitBreakerDefaults + + +
+(Optional) +

GlobalCircuitBreakerDefaults specifies default circuit breaker budget across all services. +If defined, this will be used as the default for all services.

+
+upstreamTLS +
+ + +EnvoyTLS + + +
+(Optional) +

UpstreamTLS contains the TLS policy parameters for upstream connections

+
+

ContourConfigurationSpec +

+

+(Appears on: +ContourConfiguration, +ContourDeploymentSpec) +

+

+

ContourConfigurationSpec represents a configuration of a Contour controller. +It contains most of all the options that can be customized, the +other remaining options being command line flags.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+xdsServer +
+ + +XDSServerConfig + + +
+(Optional) +

XDSServer contains parameters for the xDS server.

+
+ingress +
+ + +IngressConfig + + +
+(Optional) +

Ingress contains parameters for ingress options.

+
+debug +
+ + +DebugConfig + + +
+(Optional) +

Debug contains parameters to enable debug logging +and debug interfaces inside Contour.

+
+health +
+ + +HealthConfig + + +
+(Optional) +

Health defines the endpoints Contour uses to serve health checks.

+

Contour’s default is { address: “0.0.0.0”, port: 8000 }.

+
+envoy +
+ + +EnvoyConfig + + +
+(Optional) +

Envoy contains parameters for Envoy as well +as how to optionally configure a managed Envoy fleet.

+
+gateway +
+ + +GatewayConfig + + +
+(Optional) +

Gateway contains parameters for the gateway-api Gateway that Contour +is configured to serve traffic.

+
+httpproxy +
+ + +HTTPProxyConfig + + +
+(Optional) +

HTTPProxy defines parameters on HTTPProxy.

+
+enableExternalNameService +
+ +bool + +
+(Optional) +

EnableExternalNameService allows processing of ExternalNameServices

+

Contour’s default is false for security reasons.

+
+globalExtAuth +
+ + +AuthorizationServer + + +
+(Optional) +

GlobalExternalAuthorization allows envoys external authorization filter +to be enabled for all virtual hosts.

+
+rateLimitService +
+ + +RateLimitServiceConfig + + +
+(Optional) +

RateLimitService optionally holds properties of the Rate Limit Service +to be used for global rate limiting.

+
+policy +
+ + +PolicyConfig + + +
+(Optional) +

Policy specifies default policy applied if not overridden by the user

+
+metrics +
+ + +MetricsConfig + + +
+(Optional) +

Metrics defines the endpoint Contour uses to serve metrics.

+

Contour’s default is { address: “0.0.0.0”, port: 8000 }.

+
+tracing +
+ + +TracingConfig + + +
+

Tracing defines properties for exporting trace data to OpenTelemetry.

+
+featureFlags +
+ + +FeatureFlags + + +
+

FeatureFlags defines toggle to enable new contour features. +Available toggles are: +useEndpointSlices - Configures contour to fetch endpoint data +from k8s endpoint slices. defaults to true, +If false then reads endpoint data from the k8s endpoints.

+
+

ContourConfigurationStatus +

+

+(Appears on: +ContourConfiguration) +

+

+

ContourConfigurationStatus defines the observed state of a ContourConfiguration resource.

+

+ + + + + + + + + + + + + +
FieldDescription
+conditions +
+ + +[]DetailedCondition + + +
+(Optional) +

Conditions contains the current status of the Contour resource.

+

Contour will update a single condition, Valid, that is in normal-true polarity.

+

Contour will not modify any other Conditions set in this block, +in case some other controller wants to add a Condition.

+
+

ContourDeploymentSpec +

+

+(Appears on: +ContourDeployment) +

+

+

ContourDeploymentSpec specifies options for how a Contour +instance should be provisioned.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+contour +
+ + +ContourSettings + + +
+(Optional) +

Contour specifies deployment-time settings for the Contour +part of the installation, i.e. the xDS server/control plane +and associated resources, including things like replica count +for the Deployment, and node placement constraints for the pods.

+
+envoy +
+ + +EnvoySettings + + +
+(Optional) +

Envoy specifies deployment-time settings for the Envoy +part of the installation, i.e. the xDS client/data plane +and associated resources, including things like the workload +type to use (DaemonSet or Deployment), node placement constraints +for the pods, and various options for the Envoy service.

+
+runtimeSettings +
+ + +ContourConfigurationSpec + + +
+(Optional) +

RuntimeSettings is a ContourConfiguration spec to be used when +provisioning a Contour instance that will influence aspects of +the Contour instance’s runtime behavior.

+
+resourceLabels +
+ +map[string]string + +
+(Optional) +

ResourceLabels is a set of labels to add to the provisioned Contour resources.

+

Deprecated: use Gateway.Spec.Infrastructure.Labels instead. This field will be +removed in a future release.

+
+

ContourDeploymentStatus +

+

+(Appears on: +ContourDeployment) +

+

+

ContourDeploymentStatus defines the observed state of a ContourDeployment resource.

+

+ + + + + + + + + + + + + +
FieldDescription
+conditions +
+ + +[]Kubernetes meta/v1.Condition + + +
+(Optional) +

Conditions describe the current conditions of the ContourDeployment resource.

+
+

ContourSettings +

+

+(Appears on: +ContourDeploymentSpec) +

+

+

ContourSettings contains settings for the Contour part of the installation, +i.e. the xDS server/control plane and associated resources.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+replicas +
+ +int32 + +
+(Optional) +

Deprecated: Use DeploymentSettings.Replicas instead.

+

Replicas is the desired number of Contour replicas. If if unset, +defaults to 2.

+

if both DeploymentSettings.Replicas and this one is set, use DeploymentSettings.Replicas.

+
+nodePlacement +
+ + +NodePlacement + + +
+(Optional) +

NodePlacement describes node scheduling configuration of Contour pods.

+
+kubernetesLogLevel +
+ +byte + +
+(Optional) +

KubernetesLogLevel Enable Kubernetes client debug logging with log level. If unset, +defaults to 0.

+
+logLevel +
+ + +LogLevel + + +
+(Optional) +

LogLevel sets the log level for Contour +Allowed values are “info”, “debug”.

+
+resources +
+ + +Kubernetes core/v1.ResourceRequirements + + +
+(Optional) +

Compute Resources required by contour container. +Cannot be updated. +More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/

+
+deployment +
+ + +DeploymentSettings + + +
+(Optional) +

Deployment describes the settings for running contour as a Deployment.

+
+podAnnotations +
+ +map[string]string + +
+(Optional) +

PodAnnotations defines annotations to add to the Contour pods. +the annotations for Prometheus will be appended or overwritten with predefined value.

+
+watchNamespaces +
+ + +[]Namespace + + +
+(Optional) +

WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance +to only watch this subset of namespaces.

+
+disabledFeatures +
+ + +[]Feature + + +
+(Optional) +

DisabledFeatures defines an array of resources that will be ignored by +contour reconciler.

+
+

CustomTag +

+

+

CustomTag defines custom tags with unique tag name +to create tags for the active span.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+tagName +
+ +string + +
+

TagName is the unique name of the custom tag.

+
+literal +
+ +string + +
+(Optional) +

Literal is a static custom tag value. +Precisely one of Literal, RequestHeaderName must be set.

+
+requestHeaderName +
+ +string + +
+(Optional) +

RequestHeaderName indicates which request header +the label value is obtained from. +Precisely one of Literal, RequestHeaderName must be set.

+
+

DaemonSetSettings +

+

+(Appears on: +EnvoySettings) +

+

+

DaemonSetSettings contains settings for DaemonSet resources.

+

+ + + + + + + + + + + + + +
FieldDescription
+updateStrategy +
+ + +Kubernetes apps/v1.DaemonSetUpdateStrategy + + +
+(Optional) +

Strategy describes the deployment strategy to use to replace existing DaemonSet pods with new pods.

+
+

DebugConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

DebugConfig contains Contour specific troubleshooting options.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+address +
+ +string + +
+(Optional) +

Defines the Contour debug address interface.

+

Contour’s default is “127.0.0.1”.

+
+port +
+ +int + +
+(Optional) +

Defines the Contour debug address port.

+

Contour’s default is 6060.

+
+

DeploymentSettings +

+

+(Appears on: +ContourSettings, +EnvoySettings) +

+

+

DeploymentSettings contains settings for Deployment resources.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+replicas +
+ +int32 + +
+

Replicas is the desired number of replicas.

+
+strategy +
+ + +Kubernetes apps/v1.DeploymentStrategy + + +
+(Optional) +

Strategy describes the deployment strategy to use to replace existing pods with new pods.

+
+

EnvoyConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

EnvoyConfig defines how Envoy is to be Configured from Contour.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+listener +
+ + +EnvoyListenerConfig + + +
+(Optional) +

Listener hold various configurable Envoy listener values.

+
+service +
+ + +NamespacedName + + +
+(Optional) +

Service holds Envoy service parameters for setting Ingress status.

+

Contour’s default is { namespace: “projectcontour”, name: “envoy” }.

+
+http +
+ + +EnvoyListener + + +
+(Optional) +

Defines the HTTP Listener for Envoy.

+

Contour’s default is { address: “0.0.0.0”, port: 8080, accessLog: “/dev/stdout” }.

+
+https +
+ + +EnvoyListener + + +
+(Optional) +

Defines the HTTPS Listener for Envoy.

+

Contour’s default is { address: “0.0.0.0”, port: 8443, accessLog: “/dev/stdout” }.

+
+health +
+ + +HealthConfig + + +
+(Optional) +

Health defines the endpoint Envoy uses to serve health checks.

+

Contour’s default is { address: “0.0.0.0”, port: 8002 }.

+
+metrics +
+ + +MetricsConfig + + +
+(Optional) +

Metrics defines the endpoint Envoy uses to serve metrics.

+

Contour’s default is { address: “0.0.0.0”, port: 8002 }.

+
+clientCertificate +
+ + +NamespacedName + + +
+(Optional) +

ClientCertificate defines the namespace/name of the Kubernetes +secret containing the client certificate and private key +to be used when establishing TLS connection to upstream +cluster.

+
+logging +
+ + +EnvoyLogging + + +
+(Optional) +

Logging defines how Envoy’s logs can be configured.

+
+defaultHTTPVersions +
+ + +[]HTTPVersionType + + +
+(Optional) +

DefaultHTTPVersions defines the default set of HTTPS +versions the proxy should accept. HTTP versions are +strings of the form “HTTP/xx”. Supported versions are +“HTTP/1.1” and “HTTP/2”.

+

Values: HTTP/1.1, HTTP/2 (default: both).

+

Other values will produce an error.

+
+timeouts +
+ + +TimeoutParameters + + +
+(Optional) +

Timeouts holds various configurable timeouts that can +be set in the config file.

+
+cluster +
+ + +ClusterParameters + + +
+(Optional) +

Cluster holds various configurable Envoy cluster values that can +be set in the config file.

+
+network +
+ + +NetworkParameters + + +
+(Optional) +

Network holds various configurable Envoy network values.

+
+

EnvoyListener +

+

+(Appears on: +EnvoyConfig) +

+

+

EnvoyListener defines parameters for an Envoy Listener.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+address +
+ +string + +
+(Optional) +

Defines an Envoy Listener Address.

+
+port +
+ +int + +
+(Optional) +

Defines an Envoy listener Port.

+
+accessLog +
+ +string + +
+(Optional) +

AccessLog defines where Envoy logs are outputted for this listener.

+
+

EnvoyListenerConfig +

+

+(Appears on: +EnvoyConfig) +

+

+

EnvoyListenerConfig hold various configurable Envoy listener values.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+useProxyProtocol +
+ +bool + +
+(Optional) +

Use PROXY protocol for all listeners.

+

Contour’s default is false.

+
+disableAllowChunkedLength +
+ +bool + +
+(Optional) +

DisableAllowChunkedLength disables the RFC-compliant Envoy behavior to +strip the “Content-Length” header if “Transfer-Encoding: chunked” is +also set. This is an emergency off-switch to revert back to Envoy’s +default behavior in case of failures. Please file an issue if failures +are encountered. +See: https://github.com/projectcontour/contour/issues/3221

+

Contour’s default is false.

+
+disableMergeSlashes +
+ +bool + +
+(Optional) +

DisableMergeSlashes disables Envoy’s non-standard merge_slashes path transformation option +which strips duplicate slashes from request URL paths.

+

Contour’s default is false.

+
+serverHeaderTransformation +
+ + +ServerHeaderTransformationType + + +
+(Optional) +

Defines the action to be applied to the Server header on the response path. +When configured as overwrite, overwrites any Server header with “envoy”. +When configured as append_if_absent, if a Server header is present, pass it through, otherwise set it to “envoy”. +When configured as pass_through, pass through the value of the Server header, and do not append a header if none is present.

+

Values: overwrite (default), append_if_absent, pass_through

+

Other values will produce an error. +Contour’s default is overwrite.

+
+connectionBalancer +
+ +string + +
+(Optional) +

ConnectionBalancer. If the value is exact, the listener will use the exact connection balancer +See https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/listener.proto#envoy-api-msg-listener-connectionbalanceconfig +for more information.

+

Values: (empty string): use the default ConnectionBalancer, exact: use the Exact ConnectionBalancer.

+

Other values will produce an error.

+
+maxRequestsPerConnection +
+ +uint32 + +
+(Optional) +

Defines the maximum requests for downstream connections. If not specified, there is no limit. +see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-msg-config-core-v3-httpprotocoloptions +for more information.

+
+per-connection-buffer-limit-bytes +
+ +uint32 + +
+(Optional) +

Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. +If unspecified, an implementation defined default is applied (1MiB). +see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener.proto#envoy-v3-api-field-config-listener-v3-listener-per-connection-buffer-limit-bytes +for more information.

+
+tls +
+ + +EnvoyTLS + + +
+(Optional) +

TLS holds various configurable Envoy TLS listener values.

+
+socketOptions +
+ + +SocketOptions + + +
+(Optional) +

SocketOptions defines configurable socket options for the listeners. +Single set of options are applied to all listeners.

+
+maxRequestsPerIOCycle +
+ +uint32 + +
+(Optional) +

Defines the limit on number of HTTP requests that Envoy will process from a single +connection in a single I/O cycle. Requests over this limit are processed in subsequent +I/O cycles. Can be used as a mitigation for CVE-2023-44487 when abusive traffic is +detected. Configures the http.max_requests_per_io_cycle Envoy runtime setting. The default +value when this is not set is no limit.

+
+httpMaxConcurrentStreams +
+ +uint32 + +
+(Optional) +

Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS Envoy will advertise in the +SETTINGS frame in HTTP/2 connections and the limit for concurrent streams allowed +for a peer on a single HTTP/2 connection. It is recommended to not set this lower +than 100 but this field can be used to bound resource usage by HTTP/2 connections +and mitigate attacks like CVE-2023-44487. The default value when this is not set is +unlimited.

+
+maxConnectionsPerListener +
+ +uint32 + +
+(Optional) +

Defines the limit on number of active connections to a listener. The limit is applied +per listener. The default value when this is not set is unlimited.

+
+

EnvoyLogging +

+

+(Appears on: +EnvoyConfig) +

+

+

EnvoyLogging defines how Envoy’s logs can be configured.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+accessLogFormat +
+ + +AccessLogType + + +
+(Optional) +

AccessLogFormat sets the global access log format.

+

Values: envoy (default), json.

+

Other values will produce an error.

+
+accessLogFormatString +
+ +string + +
+(Optional) +

AccessLogFormatString sets the access log format when format is set to envoy. +When empty, Envoy’s default format is used.

+
+accessLogJSONFields +
+ + +AccessLogJSONFields + + +
+(Optional) +

AccessLogJSONFields sets the fields that JSON logging will +output when AccessLogFormat is json.

+
+accessLogLevel +
+ + +AccessLogLevel + + +
+(Optional) +

AccessLogLevel sets the verbosity level of the access log.

+

Values: info (default, all requests are logged), error (all non-success requests, i.e. 300+ response code, are logged), critical (all 5xx requests are logged) and disabled.

+

Other values will produce an error.

+
+

EnvoySettings +

+

+(Appears on: +ContourDeploymentSpec) +

+

+

EnvoySettings contains settings for the Envoy part of the installation, +i.e. the xDS client/data plane and associated resources.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+workloadType +
+ + +WorkloadType + + +
+(Optional) +

WorkloadType is the type of workload to install Envoy +as. Choices are DaemonSet and Deployment. If unset, defaults +to DaemonSet.

+
+replicas +
+ +int32 + +
+(Optional) +

Deprecated: Use DeploymentSettings.Replicas instead.

+

Replicas is the desired number of Envoy replicas. If WorkloadType +is not “Deployment”, this field is ignored. Otherwise, if unset, +defaults to 2.

+

if both DeploymentSettings.Replicas and this one is set, use DeploymentSettings.Replicas.

+
+networkPublishing +
+ + +NetworkPublishing + + +
+

NetworkPublishing defines how to expose Envoy to a network.

+
+nodePlacement +
+ + +NodePlacement + + +
+(Optional) +

NodePlacement describes node scheduling configuration of Envoy pods.

+
+extraVolumes +
+ + +[]Kubernetes core/v1.Volume + + +
+(Optional) +

ExtraVolumes holds the extra volumes to add.

+
+extraVolumeMounts +
+ + +[]Kubernetes core/v1.VolumeMount + + +
+(Optional) +

ExtraVolumeMounts holds the extra volume mounts to add (normally used with extraVolumes).

+
+podAnnotations +
+ +map[string]string + +
+(Optional) +

PodAnnotations defines annotations to add to the Envoy pods. +the annotations for Prometheus will be appended or overwritten with predefined value.

+
+resources +
+ + +Kubernetes core/v1.ResourceRequirements + + +
+(Optional) +

Compute Resources required by envoy container. +Cannot be updated. +More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/

+
+logLevel +
+ + +LogLevel + + +
+(Optional) +

LogLevel sets the log level for Envoy. +Allowed values are “trace”, “debug”, “info”, “warn”, “error”, “critical”, “off”.

+
+daemonSet +
+ + +DaemonSetSettings + + +
+(Optional) +

DaemonSet describes the settings for running envoy as a DaemonSet. +if WorkloadType is Deployment,it’s must be nil

+
+deployment +
+ + +DeploymentSettings + + +
+(Optional) +

Deployment describes the settings for running envoy as a Deployment. +if WorkloadType is DaemonSet,it’s must be nil

+
+baseID +
+ +int32 + +
+(Optional) +

The base ID to use when allocating shared memory regions. +if Envoy needs to be run multiple times on the same machine, each running Envoy will need a unique base ID +so that the shared memory regions do not conflict. +defaults to 0.

+
+overloadMaxHeapSize +
+ +uint64 + +
+(Optional) +

OverloadMaxHeapSize defines the maximum heap memory of the envoy controlled by the overload manager. +When the value is greater than 0, the overload manager is enabled, +and when envoy reaches 95% of the maximum heap size, it performs a shrink heap operation, +When it reaches 98% of the maximum heap size, Envoy Will stop accepting requests. +More info: https://projectcontour.io/docs/main/config/overload-manager/

+
+

EnvoyTLS +

+

+(Appears on: +ClusterParameters, +EnvoyListenerConfig) +

+

+

EnvoyTLS describes tls parameters for Envoy listneners.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+minimumProtocolVersion +
+ +string + +
+(Optional) +

MinimumProtocolVersion is the minimum TLS version this vhost should +negotiate.

+

Values: 1.2 (default), 1.3.

+

Other values will produce an error.

+
+maximumProtocolVersion +
+ +string + +
+(Optional) +

MaximumProtocolVersion is the maximum TLS version this vhost should +negotiate.

+

Values: 1.2, 1.3(default).

+

Other values will produce an error.

+
+cipherSuites +
+ +[]string + +
+(Optional) +

CipherSuites defines the TLS ciphers to be supported by Envoy TLS +listeners when negotiating TLS 1.2. Ciphers are validated against the +set that Envoy supports by default. This parameter should only be used +by advanced users. Note that these will be ignored when TLS 1.3 is in +use.

+

This field is optional; when it is undefined, a Contour-managed ciphersuite list +will be used, which may be updated to keep it secure.

+

Contour’s default list is: +- “[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305]” +- “[ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]” +- “ECDHE-ECDSA-AES256-GCM-SHA384” +- “ECDHE-RSA-AES256-GCM-SHA384”

+

Ciphers provided are validated against the following list: +- “[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305]” +- “[ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]” +- “ECDHE-ECDSA-AES128-GCM-SHA256” +- “ECDHE-RSA-AES128-GCM-SHA256” +- “ECDHE-ECDSA-AES128-SHA” +- “ECDHE-RSA-AES128-SHA” +- “AES128-GCM-SHA256” +- “AES128-SHA” +- “ECDHE-ECDSA-AES256-GCM-SHA384” +- “ECDHE-RSA-AES256-GCM-SHA384” +- “ECDHE-ECDSA-AES256-SHA” +- “ECDHE-RSA-AES256-SHA” +- “AES256-GCM-SHA384” +- “AES256-SHA”

+

Contour recommends leaving this undefined unless you are sure you must.

+

See: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#extensions-transport-sockets-tls-v3-tlsparameters +Note: This list is a superset of what is valid for stock Envoy builds and those using BoringSSL FIPS.

+
+

ExtensionProtocolVersion +(string alias)

+

+(Appears on: +ExtensionServiceSpec) +

+

+

ExtensionProtocolVersion is the version of the GRPC protocol used +to access extension services. The only version currently supported +is “v3”.

+

+ + + + + + + + + + + + +
ValueDescription

"v2"

SupportProtocolVersion2 requests the “v2” support protocol version.

+

Deprecated: this protocol version is no longer supported and the +constant is retained for backwards compatibility only.

+

"v3"

SupportProtocolVersion3 requests the “v3” support protocol version.

+
+

ExtensionServiceSpec +

+

+(Appears on: +ExtensionService) +

+

+

ExtensionServiceSpec defines the desired state of an ExtensionService resource.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+services +
+ + +[]ExtensionServiceTarget + + +
+

Services specifies the set of Kubernetes Service resources that +receive GRPC extension API requests. +If no weights are specified for any of the entries in +this array, traffic will be spread evenly across all the +services. +Otherwise, traffic is balanced proportionally to the +Weight field in each entry.

+
+validation +
+ + +UpstreamValidation + + +
+(Optional) +

UpstreamValidation defines how to verify the backend service’s certificate

+
+protocol +
+ +string + +
+(Optional) +

Protocol may be used to specify (or override) the protocol used to reach this Service. +Values may be h2 or h2c. If omitted, protocol-selection falls back on Service annotations.

+
+loadBalancerPolicy +
+ + +LoadBalancerPolicy + + +
+(Optional) +

The policy for load balancing GRPC service requests. Note that the +Cookie and RequestHash load balancing strategies cannot be used +here.

+
+timeoutPolicy +
+ + +TimeoutPolicy + + +
+(Optional) +

The timeout policy for requests to the services.

+
+protocolVersion +
+ + +ExtensionProtocolVersion + + +
+(Optional) +

This field sets the version of the GRPC protocol that Envoy uses to +send requests to the extension service. Since Contour always uses the +v3 Envoy API, this is currently fixed at “v3”. However, other +protocol options will be available in future.

+
+

ExtensionServiceStatus +

+

+(Appears on: +ExtensionService) +

+

+

ExtensionServiceStatus defines the observed state of an +ExtensionService resource.

+

+ + + + + + + + + + + + + +
FieldDescription
+conditions +
+ + +[]DetailedCondition + + +
+(Optional) +

Conditions contains the current status of the ExtensionService resource.

+

Contour will update a single condition, Valid, that is in normal-true polarity.

+

Contour will not modify any other Conditions set in this block, +in case some other controller wants to add a Condition.

+
+

ExtensionServiceTarget +

+

+(Appears on: +ExtensionServiceSpec) +

+

+

ExtensionServiceTarget defines an Kubernetes Service to target with +extension service traffic.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+

Name is the name of Kubernetes service that will accept service +traffic.

+
+port +
+ +int + +
+

Port (defined as Integer) to proxy traffic to since a service can have multiple defined.

+
+weight +
+ +uint32 + +
+(Optional) +

Weight defines proportion of traffic to balance to the Kubernetes Service.

+
+

FeatureFlags +([]string alias)

+

+(Appears on: +ContourConfigurationSpec) +

+

+

FeatureFlags defines the set of feature flags +to toggle new contour features.

+

+

GatewayConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

GatewayConfig holds the config for Gateway API controllers.

+

+ + + + + + + + + + + + + +
FieldDescription
+gatewayRef +
+ + +NamespacedName + + +
+

GatewayRef defines the specific Gateway that this Contour +instance corresponds to.

+
+

GlobalCircuitBreakerDefaults +

+

+(Appears on: +ClusterParameters) +

+

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+maxConnections +
+ +uint32 + +
+(Optional) +

The maximum number of connections that a single Envoy instance allows to the Kubernetes Service; defaults to 1024.

+
+maxPendingRequests +
+ +uint32 + +
+(Optional) +

The maximum number of pending requests that a single Envoy instance allows to the Kubernetes Service; defaults to 1024.

+
+maxRequests +
+ +uint32 + +
+(Optional) +

The maximum parallel requests a single Envoy instance allows to the Kubernetes Service; defaults to 1024

+
+maxRetries +
+ +uint32 + +
+(Optional) +

The maximum number of parallel retries a single Envoy instance allows to the Kubernetes Service; defaults to 3.

+
+

HTTPProxyConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

HTTPProxyConfig defines parameters on HTTPProxy.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+disablePermitInsecure +
+ +bool + +
+(Optional) +

DisablePermitInsecure disables the use of the +permitInsecure field in HTTPProxy.

+

Contour’s default is false.

+
+rootNamespaces +
+ +[]string + +
+(Optional) +

Restrict Contour to searching these namespaces for root ingress routes.

+
+fallbackCertificate +
+ + +NamespacedName + + +
+(Optional) +

FallbackCertificate defines the namespace/name of the Kubernetes secret to +use as fallback when a non-SNI request is received.

+
+

HTTPVersionType +(string alias)

+

+(Appears on: +EnvoyConfig) +

+

+

HTTPVersionType is the name of a supported HTTP version.

+

+ + + + + + + + + + + + +
ValueDescription

"HTTP/1.1"

HTTPVersion1 is the name of the HTTP/1.1 version.

+

"HTTP/2"

HTTPVersion2 is the name of the HTTP/2 version.

+
+

HeadersPolicy +

+

+(Appears on: +PolicyConfig) +

+

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+set +
+ +map[string]string + +
+(Optional) +
+remove +
+ +[]string + +
+(Optional) +
+

HealthConfig +

+

+(Appears on: +ContourConfigurationSpec, +EnvoyConfig) +

+

+

HealthConfig defines the endpoints to enable health checks.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+address +
+ +string + +
+(Optional) +

Defines the health address interface.

+
+port +
+ +int + +
+(Optional) +

Defines the health port.

+
+

IngressConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

IngressConfig defines ingress specific config items.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+classNames +
+ +[]string + +
+(Optional) +

Ingress Class Names Contour should use.

+
+statusAddress +
+ +string + +
+(Optional) +

Address to set in Ingress object status.

+
+

LogLevel +(string alias)

+

+(Appears on: +ContourSettings, +EnvoySettings) +

+

+

LogLevel is the logging levels available.

+

+ + + + + + + + + + + + + + + + + + + + + + +
ValueDescription

"critical"

CriticalLog sets the log level for Envoy to critical.

+

"debug"

DebugLog sets the log level for Contour/Envoy to debug.

+

"error"

ErrorLog sets the log level for Envoy to error.

+

"info"

InfoLog sets the log level for Contour/Envoy to info.

+

"off"

OffLog disable logging for Envoy.

+

"trace"

TraceLog sets the log level for Envoy to trace.

+

"warn"

WarnLog sets the log level for Envoy to warn.

+
+

MetricsConfig +

+

+(Appears on: +ContourConfigurationSpec, +EnvoyConfig) +

+

+

MetricsConfig defines the metrics endpoint.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+address +
+ +string + +
+(Optional) +

Defines the metrics address interface.

+
+port +
+ +int + +
+(Optional) +

Defines the metrics port.

+
+tls +
+ + +MetricsTLS + + +
+(Optional) +

TLS holds TLS file config details. +Metrics and health endpoints cannot have same port number when metrics is served over HTTPS.

+
+

MetricsTLS +

+

+(Appears on: +MetricsConfig) +

+

+

TLS holds TLS file config details.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+caFile +
+ +string + +
+(Optional) +

CA filename.

+
+certFile +
+ +string + +
+(Optional) +

Client certificate filename.

+
+keyFile +
+ +string + +
+(Optional) +

Client key filename.

+
+

NamespacedName +

+

+(Appears on: +EnvoyConfig, +GatewayConfig, +HTTPProxyConfig, +RateLimitServiceConfig, +TracingConfig) +

+

+

NamespacedName defines the namespace/name of the Kubernetes resource referred from the config file. +Used for Contour config YAML file parsing, otherwise we could use K8s types.NamespacedName.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+name +
+ +string + +
+
+namespace +
+ +string + +
+
+

NetworkParameters +

+

+(Appears on: +EnvoyConfig) +

+

+

NetworkParameters hold various configurable network values.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+numTrustedHops +
+ +uint32 + +
+(Optional) +

XffNumTrustedHops defines the number of additional ingress proxy hops from the +right side of the x-forwarded-for HTTP header to trust when determining the origin +client’s IP address.

+

See https://www.envoyproxy.io/docs/envoy/v1.17.0/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto?highlight=xff_num_trusted_hops +for more information.

+

Contour’s default is 0.

+
+adminPort +
+ +int + +
+(Optional) +

Configure the port used to access the Envoy Admin interface. +If configured to port “0” then the admin interface is disabled.

+

Contour’s default is 9001.

+
+

NetworkPublishing +

+

+(Appears on: +EnvoySettings) +

+

+

NetworkPublishing defines the schema for publishing to a network.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+type +
+ + +NetworkPublishingType + + +
+(Optional) +

NetworkPublishingType is the type of publishing strategy to use. Valid values are:

+
    +
  • LoadBalancerService
  • +
+

In this configuration, network endpoints for Envoy use container networking. +A Kubernetes LoadBalancer Service is created to publish Envoy network +endpoints.

+

See: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer

+
    +
  • NodePortService
  • +
+

Publishes Envoy network endpoints using a Kubernetes NodePort Service.

+

In this configuration, Envoy network endpoints use container networking. A Kubernetes +NodePort Service is created to publish the network endpoints.

+

See: https://kubernetes.io/docs/concepts/services-networking/service/#nodeport

+

NOTE: +When provisioning an Envoy NodePortService, use Gateway Listeners’ port numbers to populate +the Service’s node port values, there’s no way to auto-allocate them.

+

See: https://github.com/projectcontour/contour/issues/4499

+
    +
  • ClusterIPService
  • +
+

Publishes Envoy network endpoints using a Kubernetes ClusterIP Service.

+

In this configuration, Envoy network endpoints use container networking. A Kubernetes +ClusterIP Service is created to publish the network endpoints.

+

See: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types

+

If unset, defaults to LoadBalancerService.

+
+externalTrafficPolicy +
+ + +Kubernetes core/v1.ServiceExternalTrafficPolicy + + +
+(Optional) +

ExternalTrafficPolicy describes how nodes distribute service traffic they +receive on one of the Service’s “externally-facing” addresses (NodePorts, ExternalIPs, +and LoadBalancer IPs).

+

If unset, defaults to “Local”.

+
+ipFamilyPolicy +
+ + +Kubernetes core/v1.IPFamilyPolicy + + +
+(Optional) +

IPFamilyPolicy represents the dual-stack-ness requested or required by +this Service. If there is no value provided, then this field will be set +to SingleStack. Services can be “SingleStack” (a single IP family), +“PreferDualStack” (two IP families on dual-stack configured clusters or +a single IP family on single-stack clusters), or “RequireDualStack” +(two IP families on dual-stack configured clusters, otherwise fail).

+
+serviceAnnotations +
+ +map[string]string + +
+(Optional) +

ServiceAnnotations is the annotations to add to +the provisioned Envoy service.

+
+

NetworkPublishingType +(string alias)

+

+(Appears on: +NetworkPublishing) +

+

+

NetworkPublishingType is a way to publish network endpoints.

+

+ + + + + + + + + + + + + + +
ValueDescription

"ClusterIPService"

ClusterIPServicePublishingType publishes a network endpoint using a Kubernetes +ClusterIP Service.

+

"LoadBalancerService"

LoadBalancerServicePublishingType publishes a network endpoint using a Kubernetes +LoadBalancer Service.

+

"NodePortService"

NodePortServicePublishingType publishes a network endpoint using a Kubernetes +NodePort Service.

+
+

NodePlacement +

+

+(Appears on: +ContourSettings, +EnvoySettings) +

+

+

NodePlacement describes node scheduling configuration for pods. +If nodeSelector and tolerations are specified, the scheduler will use both to +determine where to place the pod(s).

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+nodeSelector +
+ +map[string]string + +
+(Optional) +

NodeSelector is the simplest recommended form of node selection constraint +and specifies a map of key-value pairs. For the pod to be eligible +to run on a node, the node must have each of the indicated key-value pairs +as labels (it can have additional labels as well).

+

If unset, the pod(s) will be scheduled to any available node.

+
+tolerations +
+ + +[]Kubernetes core/v1.Toleration + + +
+(Optional) +

Tolerations work with taints to ensure that pods are not scheduled +onto inappropriate nodes. One or more taints are applied to a node; this +marks that the node should not accept any pods that do not tolerate the +taints.

+

The default is an empty list.

+

See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +for additional details.

+
+

PolicyConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

PolicyConfig holds default policy used if not explicitly set by the user

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+requestHeaders +
+ + +HeadersPolicy + + +
+(Optional) +

RequestHeadersPolicy defines the request headers set/removed on all routes

+
+responseHeaders +
+ + +HeadersPolicy + + +
+(Optional) +

ResponseHeadersPolicy defines the response headers set/removed on all routes

+
+applyToIngress +
+ +bool + +
+(Optional) +

ApplyToIngress determines if the Policies will apply to ingress objects

+

Contour’s default is false.

+
+

RateLimitServiceConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

RateLimitServiceConfig defines properties of a global Rate Limit Service.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+extensionService +
+ + +NamespacedName + + +
+

ExtensionService identifies the extension service defining the RLS.

+
+domain +
+ +string + +
+(Optional) +

Domain is passed to the Rate Limit Service.

+
+failOpen +
+ +bool + +
+(Optional) +

FailOpen defines whether to allow requests to proceed when the +Rate Limit Service fails to respond with a valid rate limit +decision within the timeout defined on the extension service.

+
+enableXRateLimitHeaders +
+ +bool + +
+(Optional) +

EnableXRateLimitHeaders defines whether to include the X-RateLimit +headers X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset +(as defined by the IETF Internet-Draft linked below), on responses +to clients when the Rate Limit Service is consulted for a request.

+

ref. https://tools.ietf.org/id/draft-polli-ratelimit-headers-03.html

+
+enableResourceExhaustedCode +
+ +bool + +
+(Optional) +

EnableResourceExhaustedCode enables translating error code 429 to +grpc code RESOURCE_EXHAUSTED. When disabled it’s translated to UNAVAILABLE

+
+defaultGlobalRateLimitPolicy +
+ + +GlobalRateLimitPolicy + + +
+(Optional) +

DefaultGlobalRateLimitPolicy allows setting a default global rate limit policy for every HTTPProxy. +HTTPProxy can overwrite this configuration.

+
+

ServerHeaderTransformationType +(string alias)

+

+(Appears on: +EnvoyListenerConfig) +

+

+

ServerHeaderTransformation defines the action to be applied to the Server header on the response path

+

+ + + + + + + + + + + + + + +
ValueDescription

"append_if_absent"

If no Server header is present, set it to “envoy”. +If a Server header is present, pass it through.

+

"overwrite"

Overwrite any Server header with “envoy”. +This is the default value.

+

"pass_through"

Pass through the value of the Server header, and do not append a header +if none is present.

+
+

SocketOptions +

+

+(Appears on: +EnvoyListenerConfig) +

+

+

SocketOptions defines configurable socket options for Envoy listeners.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+tos +
+ +int32 + +
+(Optional) +

Defines the value for IPv4 TOS field (including 6 bit DSCP field) for IP packets originating from Envoy listeners. +Single value is applied to all listeners. +If listeners are bound to IPv6-only addresses, setting this option will cause an error.

+
+trafficClass +
+ +int32 + +
+(Optional) +

Defines the value for IPv6 Traffic Class field (including 6 bit DSCP field) for IP packets originating from the Envoy listeners. +Single value is applied to all listeners. +If listeners are bound to IPv4-only addresses, setting this option will cause an error.

+
+

TLS +

+

+(Appears on: +XDSServerConfig) +

+

+

TLS holds TLS file config details.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+caFile +
+ +string + +
+(Optional) +

CA filename.

+
+certFile +
+ +string + +
+(Optional) +

Client certificate filename.

+
+keyFile +
+ +string + +
+(Optional) +

Client key filename.

+
+insecure +
+ +bool + +
+(Optional) +

Allow serving the xDS gRPC API without TLS.

+
+

TimeoutParameters +

+

+(Appears on: +EnvoyConfig) +

+

+

TimeoutParameters holds various configurable proxy timeout values.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+requestTimeout +
+ +string + +
+(Optional) +

RequestTimeout sets the client request timeout globally for Contour. Note that +this is a timeout for the entire request, not an idle timeout. Omit or set to +“infinity” to disable the timeout entirely.

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-request-timeout +for more information.

+
+connectionIdleTimeout +
+ +string + +
+(Optional) +

ConnectionIdleTimeout defines how long the proxy should wait while there are +no active requests (for HTTP/1.1) or streams (for HTTP/2) before terminating +an HTTP connection. Set to “infinity” to disable the timeout entirely.

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-idle-timeout +for more information.

+
+streamIdleTimeout +
+ +string + +
+(Optional) +

StreamIdleTimeout defines how long the proxy should wait while there is no +request activity (for HTTP/1.1) or stream activity (for HTTP/2) before +terminating the HTTP request or stream. Set to “infinity” to disable the +timeout entirely.

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-stream-idle-timeout +for more information.

+
+maxConnectionDuration +
+ +string + +
+(Optional) +

MaxConnectionDuration defines the maximum period of time after an HTTP connection +has been established from the client to the proxy before it is closed by the proxy, +regardless of whether there has been activity or not. Omit or set to “infinity” for +no max duration.

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-max-connection-duration +for more information.

+
+delayedCloseTimeout +
+ +string + +
+(Optional) +

DelayedCloseTimeout defines how long envoy will wait, once connection +close processing has been initiated, for the downstream peer to close +the connection before Envoy closes the socket associated with the connection.

+

Setting this timeout to ‘infinity’ will disable it, equivalent to setting it to ‘0’ +in Envoy. Leaving it unset will result in the Envoy default value being used.

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-delayed-close-timeout +for more information.

+
+connectionShutdownGracePeriod +
+ +string + +
+(Optional) +

ConnectionShutdownGracePeriod defines how long the proxy will wait between sending an +initial GOAWAY frame and a second, final GOAWAY frame when terminating an HTTP/2 connection. +During this grace period, the proxy will continue to respond to new streams. After the final +GOAWAY frame has been sent, the proxy will refuse new streams.

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-drain-timeout +for more information.

+
+connectTimeout +
+ +string + +
+(Optional) +

ConnectTimeout defines how long the proxy should wait when establishing connection to upstream service. +If not set, a default value of 2 seconds will be used.

+

See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-field-config-cluster-v3-cluster-connect-timeout +for more information.

+
+

TracingConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

TracingConfig defines properties for exporting trace data to OpenTelemetry.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+includePodDetail +
+ +bool + +
+(Optional) +

IncludePodDetail defines a flag. +If it is true, contour will add the pod name and namespace to the span of the trace. +the default is true. +Note: The Envoy pods MUST have the HOSTNAME and CONTOUR_NAMESPACE environment variables set for this to work properly.

+
+serviceName +
+ +string + +
+

ServiceName defines the name for the service. +contour’s default is contour.

+
+overallSampling +
+ +string + +
+(Optional) +

OverallSampling defines the sampling rate of trace data. +contour’s default is 100.

+
+maxPathTagLength +
+ +uint32 + +
+(Optional) +

MaxPathTagLength defines maximum length of the request path +to extract and include in the HttpUrl tag. +contour’s default is 256.

+
+customTags +
+ + +[]*github.com/projectcontour/contour/apis/projectcontour/v1alpha1.CustomTag + + +
+(Optional) +

CustomTags defines a list of custom tags with unique tag name.

+
+extensionService +
+ + +NamespacedName + + +
+

ExtensionService identifies the extension service defining the otel-collector.

+
+

WorkloadType +(string alias)

+

+(Appears on: +EnvoySettings) +

+

+

WorkloadType is the type of Kubernetes workload to use for a component.

+

+

XDSServerConfig +

+

+(Appears on: +ContourConfigurationSpec) +

+

+

XDSServerConfig holds the config for the Contour xDS server.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+type +
+ + +XDSServerType + + +
+(Optional) +

Defines the XDSServer to use for contour serve.

+

Values: envoy (default), contour (deprecated).

+

Other values will produce an error.

+
+address +
+ +string + +
+(Optional) +

Defines the xDS gRPC API address which Contour will serve.

+

Contour’s default is “0.0.0.0”.

+
+port +
+ +int + +
+(Optional) +

Defines the xDS gRPC API port which Contour will serve.

+

Contour’s default is 8001.

+
+tls +
+ + +TLS + + +
+(Optional) +

TLS holds TLS file config details.

+

Contour’s default is { caFile: “/certs/ca.crt”, certFile: “/certs/tls.cert”, keyFile: “/certs/tls.key”, insecure: false }.

+
+

XDSServerType +(string alias)

+

+(Appears on: +XDSServerConfig) +

+

+

XDSServerType is the type of xDS server implementation.

+

+ + + + + + + + + + + + +
ValueDescription

"contour"

Use Contour’s xDS server (deprecated).

+

"envoy"

Use the upstream go-control-plane-based xDS server.

+
+
+

+Generated with gen-crd-api-reference-docs. +

diff --git a/site/content/docs/1.29/config/api.md b/site/content/docs/1.29/config/api.md new file mode 100644 index 00000000000..99809537d11 --- /dev/null +++ b/site/content/docs/1.29/config/api.md @@ -0,0 +1,3 @@ +# Contour API Reference + +{{% include-html api-reference.html %}} diff --git a/site/content/docs/1.29/config/client-authorization.md b/site/content/docs/1.29/config/client-authorization.md new file mode 100644 index 00000000000..4db2b932eff --- /dev/null +++ b/site/content/docs/1.29/config/client-authorization.md @@ -0,0 +1,123 @@ +# Client Authorization + +Contour supports integrating external servers to authorize client requests. + +Envoy implements external authorization in the [ext_authz][1] filter. +This filter intercepts client requests and holds them while it sends a check +request to an external server. +The filter uses the check result to either allow the request to proceed, or to +deny or redirect the request. + +The diagram below shows the sequence of requests involved in the successful +authorization of a HTTP request: + +

+client authorization sequence diagram +

+ +The [external authorization][7] guides demonstrates how to deploy HTTP basic +authentication using Contour and [contour-authserver](https://github.com/projectcontour/contour-authserver). + +## Extension Services + +The starting point for external authorization in Contour is the +[ExtensionService][2] API. +This API creates a cluster which Envoy can use to send requests to an external server. +In principle, the Envoy cluster can be used for any purpose, but in this +document we are concerned only with how to use it as an authorization service. + +An authorization service is a gRPC service that implements the Envoy [CheckRequest][3] protocol. +Note that Contour requires the extension to implement the "v3" version of the protocol. +Contour is compatible with any authorization server that implements this protocol. + +The primary field of interest in the `ExtensionService` CRD is the +`.spec.services` field. +This field lists the Kubernetes Services that will receive the check requests. +The `.spec.services[].name` field contains the name of the Service, which must +exist in the same namespace as the `ExtensionService` object. +The `ExtensionService` object must exist in the same namespace as the +Services they target to ensure that both objects are under the same +administrative control. + +### Load Balancing for Extension Services + +An `ExtensionService` can be configured to send traffic to multiple Kubernetes Services. +In this case, requests are divided proportionally across the Services according +to the weight in the `.spec.services[].weight` field. +The service weight can be used to flexibly shift traffic between Services for +reasons like implementing blue-green deployments. +The `.spec.loadBalancerPolicy` field configures how Envoy will load balance +requests to the endpoints within each Service. + +### TLS Validation for Extension Services + +Since authorizing a client request may involve passing sensitive credentials +from a HTTP request to the authorization service, the connection to the +authorization server should be as secure as possible. +Contour defaults the `.spec.protocol` field to "h2", which configures +Envoy to use HTTP/2 over TLS for the authorization service connection. + +The [.spec.validation][4] field configures how Envoy should verify the TLS +identity of the authorization server. +This is a critical protection against accidentally sending credentials to an +imposter service and should be enabled for all production deployments. +The `.spec.validation` field should specify the expected server name +from the authorization server's TLS certificate, and the trusted CA bundle +that can be used to validate the TLS chain of trust. + +## Authorizing Virtual Hosts + +The [.spec.virtualhost.authorization][5] field in the Contour `HTTPProxy` +API connects a virtual host to an authorization server that is bound by an +`ExtensionService` object. +Each virtual host can use a different `ExtensionService`, but only one +`ExtensionService` can be used by a single virtual host. +Authorization servers can only be attached to `HTTPProxy` objects that have TLS +termination enabled. + +### Migrating from Application Authorization + +When applications perform their own authorization, migrating to centralized +authorization may need some planning. +The `.spec.virtualhost.authorization.failOpen` field controls how client +requests should be handled when the authorization server fails. +During a migration process, this can be set to `true`, so that if the +authorization server becomes unavailable, clients can gracefully fall back to +the existing application authorization mechanism. + +### Scoping Authorization Policy Settings + +It is common for services to contain some HTTP request paths that require +authorization and some that do not. +The HTTPProxy [authorization policy][6] allows authorization to be +disabled for both an entire virtual host and for specific routes. + +The initial authorization policy is set on the HTTPProxy virtual host +in the `.spec.virtualhost.authorization.authPolicy` field. +This configures whether authorization is enabled, and the default authorization policy context. +If authorization is disabled on the virtual host, it is also disabled by +default on all the routes for that virtual host that do not specify an authorization policy. +However, a route can configure its own authorization policy (in the +`.spec.routes[].authPolicy` field) that can configure whether authorization +is enabled, irrespective of the virtual host setting. + +The authorization policy context is a way to configure a set of key/value +pairs that will be sent to the authorization server with each request check +request. +The keys and values that should be specified here depend on which authorization +server has been configured. +This facility is intended for configuring authorization-specific information, such as +the basic authentication realm, or OIDC parameters. + +The initial context map can be set on the virtual host. +This sets the context keys that will be sent on every check request. +A route can overwrite the value for a context key by setting it in the +context field of authorization policy for the route. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter +[2]: api/#projectcontour.io/v1alpha1.ExtensionService +[3]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto +[4]: api/#projectcontour.io/v1.UpstreamValidation +[5]: api/#projectcontour.io/v1.AuthorizationServer +[6]: api/#projectcontour.io/v1.AuthorizationPolicy +[7]: guides/external-authorization.md diff --git a/site/content/docs/1.29/config/cookie-rewriting.md b/site/content/docs/1.29/config/cookie-rewriting.md new file mode 100644 index 00000000000..480fc34125c --- /dev/null +++ b/site/content/docs/1.29/config/cookie-rewriting.md @@ -0,0 +1,109 @@ +# Cookie Rewriting + +Contour now enables users to customize attributes on HTTP `Set-Cookie` response headers. +Application specific cookies and cookies generated by Contour's ["cookie" load balancing strategy](https://projectcontour.io/docs/v1.19.0/config/request-routing/#session-affinity) can be rewritten either per HTTPProxy `Route` or `Service`. +Users can choose to rewrite the `Path`, `Domain`, `Secure`, and `SameSite` attributes of the `Set-Cookie` header currently. +These attributes may be things an application may not be able to accurately set, without prior knowledge of how the application is deployed. +For example, if Contour is in use to rewrite the path or hostname of a request before it reaches an application backend, the application may not be able to accurately set the `Path` and `Domain` attributes in a `Set-Cookie` response header. +This feature can be used to apply security settings to ensure browsers treat generated cookies appropriately. +The `SameSite` and `Secure` attributes are currently not set by Envoy when it generates the `X-Contour-Session-Affinity`, but with this feature, users can customize this cookie further. + +## Per-Route Cookie Rewriting + +In order to implement separate cookie rewriting policies per-route, we can configure an HTTPProxy as below: + +```yaml +# cookie-rewrite-route.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: cookie-rewrite-route +spec: + virtualhost: + fqdn: cookie-rewrite-route.com + routes: + - conditions: + - prefix: /admin + services: + - name: admin-app + port: 80 + cookieRewritePolicies: + - name: X-Admin-Session + pathRewrite: + value: /admin + - conditions: + - prefix: /payments + services: + - name: payment-app + port: 80 + cookieRewritePolicies: + - name: X-User-Session + pathRewrite: + value: /payments + sameSite: Lax + - name: X-User-Data + sameSite: Lax +``` + +This HTTPProxy allows us to rewrite the `Path` attribute of the `X-Admin-Session` cookie on the `/admin` route. +In addition on the `/payments` route we rewrite the `Path` and `SameSite` attributes of the `X-User-Session` cookie and the `SameSite` attribute of the additional `X-User-Data` cookie. +If the backing services `payment-app` and `admin-app` return the specified cookies in `Set-Cookie` response headers, they will be rewritten with the values specified above. + +## Per-Service Cookie Rewriting + +Similar to the above, if we have more than one `Service` configured per `Route` but want to customize cookies separately between them we can: + +```yaml +# cookie-rewrite-service.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: cookie-rewrite-service +spec: + virtualhost: + fqdn: cookie-rewrite-service.com + routes: + - conditions: + - prefix: / + services: + - name: backend-1 + port: 80 + cookieRewritePolicies: + - name: X-User-Data-1 + domainRewrite: + value: cookie-rewrite-service.com + - name: backend-2 + port: 80 + cookieRewritePolicies: + - name: X-User-Data-2 + domainRewrite: + value: cookie-rewrite-service.com +``` + +## Rewriting Contour Session Affinity Cookie + +As mentioned above, users can use Contour's cookie load balancing strategy to enable session affinity. +Envoy generates a pretty bare-bones cookie but Contour's cookie rewriting feature can be used to customize this cookie to add security attributes: + +```yaml +# cookie-rewrite-session-affinity.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: cookie-rewrite-session-affinity +spec: + virtualhost: + fqdn: cookie-rewrite-session-affinity.com + routes: + - conditions: + - prefix: / + services: + - name: backend + port: 80 + loadBalancerPolicy: + strategy: Cookie + cookieRewritePolicies: + - name: X-Contour-Session-Affinity + sameSite: Strict + secure: true +``` diff --git a/site/content/docs/1.29/config/cors.md b/site/content/docs/1.29/config/cors.md new file mode 100644 index 00000000000..8f468aeaec7 --- /dev/null +++ b/site/content/docs/1.29/config/cors.md @@ -0,0 +1,82 @@ +# CORS + +A CORS (Cross-origin resource sharing) policy can be set for a HTTPProxy in order to allow cross-domain requests for trusted sources. +If a policy is set, it will be applied to all the routes of the virtual host. + +Contour allows configuring the headers involved in responses to cross-domain requests. +These include the `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, `Access-Control-Expose-Headers`, `Access-Control-Max-Age`, `Access-Control-Allow-Private-Network` and `Access-Control-Allow-Credentials` headers in responses. + +In this example, cross-domain requests will be allowed for any domain (note the `*` value), with the methods `GET`, `POST`, or `OPTIONS`. +Headers `Authorization` and `Cache-Control` will be passed to the upstream server and headers `Content-Length` and `Content-Range` will be made available to the cross-origin request client. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: cors-example +spec: + virtualhost: + fqdn: www.example.com + corsPolicy: + allowCredentials: true + allowPrivateNetwork: true + allowOrigin: + - "*" # allows any origin + allowMethods: + - GET + - POST + - OPTIONS + allowHeaders: + - authorization + - cache-control + exposeHeaders: + - Content-Length + - Content-Range + maxAge: "10m" # preflight requests can be cached for 10 minutes. + routes: + - conditions: + - prefix: / + services: + - name: cors-example + port: 80 +``` + +The `allowOrigin` list may also be configured with exact origin matches or regex patterns. +In the following example, cross-domain requests must originate from the domain `https://client.example.com` or domains that match the regex `http[s]?:\/\/some-site-[a-z0-9]+\.example\.com` (e.g. request with `Origin` header `https://some-site-abc456.example.com`) + +*Note:* Patterns for matching `Origin` headers must be valid regex, simple "globbing" patterns (e.g. `*.foo.com`) will not be accepted or may produce incorrect matches. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: cors-example +spec: + virtualhost: + fqdn: www.example.com + corsPolicy: + allowCredentials: true + allowOrigin: + - https://client.example.com + - http[s]?:\/\/some-site-[a-z0-9]+\.example\.com + allowMethods: + - GET + - POST + - OPTIONS + allowHeaders: + - authorization + - cache-control + exposeHeaders: + - Content-Length + - Content-Range + maxAge: "10m" + routes: + - conditions: + - prefix: / + services: + - name: cors-example + port: 80 +``` + +`MaxAge` durations are expressed in the Go [duration format](https://godoc.org/time#ParseDuration). +Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". Only positive values are allowed and 0 disables the cache requiring a preflight `OPTIONS` check for all cross-origin requests. diff --git a/site/content/docs/1.29/config/external-service-routing.md b/site/content/docs/1.29/config/external-service-routing.md new file mode 100644 index 00000000000..7da431dd06b --- /dev/null +++ b/site/content/docs/1.29/config/external-service-routing.md @@ -0,0 +1,47 @@ +# External Service Routing + +HTTPProxy supports routing traffic to `ExternalName` service types, but this is disabled by default, as it can lead +to inadvertent exposure of the Envoy Admin UI, allowing remote shutdown and restart of Envoy. +Please see [this security advisory](https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc) for all the details. +It can also be used to expose services in namespaces a user does not have access to, using an ExternalName of `service.namespace.svc.cluster.local`. +Please see [this Kubernetes security advisory](https://github.com/kubernetes/kubernetes/issues/103675) for more details. + +We do *not* recommend enabling ExternalName Services without a strong use case, and understanding of the security implications. + +However, To enable ExternalName processing, you must set the `enableExternalNameService` configuration file setting to `true`. +This will allow the following configuration to be valid. + +## ExternalName Support + +Contour looks at the `spec.externalName` field of the service and configures the route to use that DNS name instead of utilizing EDS. + +Note that hostnames of `localhost` or some other synonyms will be rejected (because of the aforementioned security issues). + +There's nothing specific in the HTTPProxy object that needs to be configured other than referencing a service of type `ExternalName`. +HTTPProxy supports the `requestHeadersPolicy` field to rewrite the `Host` header after first handling a request and before proxying to an upstream service. +This field can be used to ensure that the forwarded HTTP request contains the hostname that the external resource is expecting. + +_**Note:** The ports are required to be specified._ + +```yaml +# httpproxy-externalname.yaml +apiVersion: v1 +kind: Service +metadata: + labels: + run: externaldns + name: externaldns + namespace: default +spec: + externalName: foo-basic.bar.com + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 80 + type: ExternalName +``` + +To proxy to another resource outside the cluster (e.g. A hosted object store bucket for example), configure that external resource in a service type `externalName`. +Then define a `requestHeadersPolicy` which replaces the `Host` header with the value of the external name service defined previously. +Finally, if the upstream service is served over TLS, set the `protocol` field on the service to `tls` or annotate the external name service with: `projectcontour.io/upstream-protocol.tls: 443,https`, assuming your service had a port 443 and name `https`. diff --git a/site/content/docs/1.29/config/fundamentals.md b/site/content/docs/1.29/config/fundamentals.md new file mode 100644 index 00000000000..0bdac65f77f --- /dev/null +++ b/site/content/docs/1.29/config/fundamentals.md @@ -0,0 +1,197 @@ +# HTTPProxy Fundamentals + +The [Ingress][1] object was added to Kubernetes in version 1.1 to describe properties of a cluster-wide reverse HTTP proxy. +Since that time, the Ingress API has remained relatively unchanged, and the need to express implementation-specific capabilities has inspired an [explosion of annotations][2]. + +The goal of the HTTPProxy Custom Resource Definition (CRD) is to expand upon the functionality of the Ingress API to allow for a richer user experience as well addressing the limitations of the latter's use in multi tenant environments. + +## Key HTTPProxy Benefits + +- Safely supports multi-team Kubernetes clusters, with the ability to limit which Namespaces may configure virtual hosts and TLS credentials. +- Enables including of routing configuration for a path or domain from another HTTPProxy, possibly in another Namespace. +- Accepts multiple services within a single route and load balances traffic across them. +- Natively allows defining service weighting and load balancing strategy without annotations. +- Validation of HTTPProxy objects at creation time and status reporting for post-creation validity. + +## Ingress to HTTPProxy + +A minimal Ingress object might look like: + +```yaml +# ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: basic +spec: + rules: + - host: foo-basic.bar.com + http: + paths: + - backend: + service: + name: s1 + port: + number: 80 + pathType: Prefix +``` + +This Ingress object, named `basic`, will route incoming HTTP traffic with a `Host:` header for `foo-basic.bar.com` to a Service named `s1` on port `80`. +Implementing similar behavior using an HTTPProxy looks like this: + +```yaml +# httpproxy.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: basic +spec: + virtualhost: + fqdn: foo-basic.bar.com + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 +``` + +**Lines 1-5**: As with all other Kubernetes objects, an HTTPProxy needs apiVersion, kind, and metadata fields. + +**Lines 7-8**: The presence of the `virtualhost` field indicates that this is a root HTTPProxy that is the top level entry point for this domain. + + +## Interacting with HTTPProxies + +As with all Kubernetes objects, you can use `kubectl` to create, list, describe, edit, and delete HTTPProxy CRDs. + +Creating an HTTPProxy: + +```bash +$ kubectl create -f basic.httpproxy.yaml +httpproxy "basic" created +``` + +Listing HTTPProxies: + +```bash +$ kubectl get httpproxy +NAME AGE +basic 24s +``` + +Describing HTTPProxy: + +```bash +$ kubectl describe httpproxy basic +Name: basic +Namespace: default +Labels: +API Version: projectcontour.io/v1 +Kind: HTTPProxy +Metadata: + Cluster Name: + Creation Timestamp: 2019-07-05T19:26:54Z + Resource Version: 19373717 + Self Link: /apis/projectcontour.io/v1/namespaces/default/httpproxy/basic + UID: 6036a9d7-8089-11e8-ab00-f80f4182762e +Spec: + Routes: + Conditions: + Prefix: / + Services: + Name: s1 + Port: 80 + Virtualhost: + Fqdn: foo-basic.bar.com +Events: +``` + +Deleting HTTPProxies: + +```bash +$ kubectl delete httpproxy basic +httpproxy "basic" deleted +``` + +## Status Reporting + +There are many misconfigurations that could cause an HTTPProxy or delegation to be invalid. +Contour will make its best effort to process even partially valid configuration and allow traffic to be served for the valid parts. +To aid users in resolving any issues, Contour updates a `status` field in all HTTPProxy objects. + +If an HTTPProxy object is valid, it will have a status property that looks like this: + +```yaml +status: + currentStatus: valid + description: valid HTTPProxy +``` + +If the HTTPProxy is invalid, the `currentStatus` field will be `invalid` and the `description` field will provide a description of the issue. + +As an example, if an HTTPProxy object has specified a negative value for weighting, the HTTPProxy status will be: + +```yaml +status: + currentStatus: invalid + description: "route '/foo': service 'home': weight must be greater than or equal to zero" +``` + +Some examples of invalid configurations that Contour provides statuses for: + +- Negative weight provided in the route definition. +- Invalid port number provided for service. +- Prefix in parent does not match route in delegated route. +- Root HTTPProxy created in a namespace other than the allowed root namespaces. +- A given Route of an HTTPProxy both delegates to another HTTPProxy and has a list of services. +- Orphaned route. +- Delegation chain produces a cycle. +- Root HTTPProxy does not specify fqdn. +- Multiple prefixes cannot be specified on the same set of route conditions. +- Multiple header conditions of type "exact match" with the same header key. +- Contradictory header conditions on a route, e.g. a "contains" and "notcontains" condition for the same header and value. + +Invalid configuration is ignored and will be not used in the ingress routing configuration. +Envoy will respond with an error when HTTP request is received on route with invalid configuration on following cases: + +* `502 Bad Gateway` response is sent when HTTPProxy has an include that refers to an HTTPProxy that does not exist. +* `503 Service Unavailable` response is sent when HTTPProxy refers to a service that does not exist. + +### Example + +Following example has two routes: the first one is valid, the second one refers to a service that does not exist. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: multiple-routes-with-a-missing-service +spec: + virtualhost: + fqdn: www.example.com + routes: + - conditions: + - prefix: / + services: + - name: valid-service + port: 80 + - conditions: + - prefix: /subpage + services: + - name: service-that-does-not-exist + port: 80 +``` + +The `HTTPProxy` will have condition `Valid=false` with detailed error message: `Spec.Routes unresolved service reference: service "default/service-that-does-not-exist" not found`. +Requests received for `http://www.example.com/` will be forwarded to `valid-service` but requests received for `http://www.example.com/subpage` will result in error `503 Service Unavailable` response from Envoy. + +## HTTPProxy API Specification + +The full HTTPProxy specification is described in detail in the [API documentation][4]. +There are a number of working examples of HTTPProxy objects in the [`examples/example-workload`][3] directory of the Contour Github repository. + + [1]: https://kubernetes.io/docs/concepts/services-networking/ingress/ + [2]: https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md + [3]: {{< param github_url>}}/tree/{{< param branch >}}/examples/example-workload/httpproxy + [4]: api.md diff --git a/site/content/docs/1.29/config/gateway-api.md b/site/content/docs/1.29/config/gateway-api.md new file mode 100644 index 00000000000..605103dc7e3 --- /dev/null +++ b/site/content/docs/1.29/config/gateway-api.md @@ -0,0 +1,217 @@ +# Gateway API + +## Introduction + +[Gateway API][1] is an open source project managed by the SIG Network community. +It is a collection of resources that model service networking in Kubernetes. +These resources - GatewayClass, Gateway, HTTPRoute, TCPRoute, Service, etc - aim to evolve Kubernetes service networking through expressive, extensible, and role-oriented interfaces that are implemented by many vendors and have broad industry support. + +Contour implements Gateway API in addition to supporting HTTPProxy and Ingress. +In particular, Contour aims to support all [core and extended features][2] in Gateway API. + +Gateway API has a comprehensive [website and docs][1], so this document focuses primarily on unique aspects of Contour's Gateway API implementation, rather than attempting to reproduce all of the content available on the Gateway API website. +The reader is suggested to familiarize themselves with the basics of Gateway API before continuing with this doc. + +In Contour's Gateway API implementation, a Gateway corresponds 1:1 with a single deployment of Contour + Envoy. +In other words, each Gateway has its own control plane (Contour) and data plane (Envoy). + +The remainder of this document delves into more detail regarding configuration options when using Contour with Gateway API. +If you are looking for a way to get started with Gateway API and Contour, see the [Gateway API guide][12], a step-by-step tutorial on getting Contour installed with Gateway API and using it to route traffic to a service. + +## Enabling Gateway API in Contour + +There are two ways to deploy Contour with Gateway API support: **static** provisioning and **dynamic** provisioning. + +In **static** provisioning, the platform operator defines a `Gateway` resource, and then manually deploys a Contour instance corresponding to that `Gateway` resource. +It is up to the platform operator to ensure that all configuration matches between the `Gateway` and the Contour/Envoy resources. +Contour will then process that `Gateway` and its routes. + +In **dynamic** provisioning, the platform operator first deploys Contour's Gateway provisioner. Then, the platform operator defines a `Gateway` resource, and the provisioner automatically deploys a Contour instance that corresponds to the `Gateway's` configuration and will process that `Gateway` and its routes. + +Static provisioning makes sense for users who: +- prefer the traditional model of deploying Contour +- have only a single Gateway +- want to use just the standard listener ports (80/443) +- have highly customized YAML for deploying Contour. + +Dynamic provisioning makes sense for users who: +- have many Gateways +- want to use additional listener ports +- prefer a simple declarative API for provisioning Contour instances +- want a fully conformant Gateway API implementation + +### Static Provisioning + +To statically provision Contour with Gateway API enabled: + +1. Install the [Gateway API experimental channel][3]. +1. Create a GatewayClass, with a controller name of `projectcontour.io/gateway-controller`. +1. Create a Gateway using the above GatewayClass. +1. In the Contour config file, add a reference to the above Gateway via `gateway.gatewayRef` (see https://projectcontour.io/docs/1.25/configuration/#gateway-configuration) +1. Install Contour using the above config file. + +Contour provides an example manifest for this at https://projectcontour.io/quickstart/contour-gateway.yaml. + +### Dynamic Provisioning + +To dynamically provision Contour with Gateway API enabled: + +1. Install the [Contour Gateway Provisioner][9], which includes the Gateway API experimental channel. +1. Create a GatewayClass, with a controller name of `projectcontour.io/gateway-controller`. +1. Create a Gateway using the above GatewayClass. + +The Contour Gateway Provisioner will deploy an instance of Contour in the Gateway's namespace implementing the Gateway spec. + +**Note:** Gateway names must be 63 characters or shorter, to avoid issues when generating dependent resources. See [projectcontour/contour#5970][13] and [kubernetes-sigs/gateway-api#2592][14] for more information. + +## Gateway Listeners + +Each unique Gateway Listener port requires the Envoy service to expose that port, and to map it to an underlying port in the Envoy daemonset/deployment that Envoy is configured to listen on. +For example, the following Gateway Listener configuration (abridged) requires service ports of 80 and 443, mapped to underlying container ports 8080 and 8443: + +```yaml +listeners: +- name: http + protocol: HTTP + port: 80 +- name: https + protocol: HTTPS + port: 443 +``` + +In dynamic provisioning, the Contour Gateway Provisioner will continuously ensure that the Envoy service and daemonset/deployment are kept in sync with the Gateway Listener configuration. +In static provisioning, it is up to the platform operator to keep the Envoy resources in sync with the Gateway Listeners. + +To get from the Gateway Listener port to the port that Envoy will be configured to listen on, i.e. the container port: +- add 8000 to the Listener port number +- if the result is greater than 65535, subtract 65535 +- if the result is less than or equal to 1023, add 1023. + +Note that, in rare corner cases, it's possible to have port conflicts. +Check the Gateway status to ensure that Listeners have been properly provisioned. + +## Routing + +Gateway API defines multiple route types. +Each route type is appropriate for a different type of traffic being proxied to a backend service. +Contour implements `HTTPRoute`, `TLSRoute`, `GRPCRoute` and `TCPRoute`. +The details of each of these route types are covered in extensive detail on the Gateway API website; the [route resources overview][11] is a good place to start learning about them. + +### Routing with HTTPProxy or Ingress + +When Gateway API is enabled in Contour, it's still possible to use HTTPProxy or Ingress to define routes, with some limitations. +This is useful for users who: +- are in the process of migrating to Gateway API +- want to use the Contour Gateway Provisioner for dynamic provisioning, but need the advanced features of HTTPProxy + +To use HTTPProxy or Ingress with Gateway API, define a Gateway with the following Listeners: + +```yaml +listeners: +- name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +- name: https + protocol: projectcontour.io/https + port: 443 + allowedRoutes: + namespaces: + from: All +``` + +Note that for the second Listener, a Contour-specific protocol is used, and no TLS details are specified. +Instead, TLS details continue to be configured on the HTTPProxy or Ingress resource. + +This is an area of active development and further work will be done in upcoming releases to better support migrations and mixed modes of operation. + +## Contour Gateway Provisioner + +### Customizing a GatewayClass + +Gateway API [supports attaching parameters to a GatewayClass][5], which can customize the Gateways that are provisioned for that GatewayClass. + +Contour defines a CRD called `ContourDeployment`, which can be used as `GatewayClass` parameters. + +A simple example of a parameterized Contour GatewayClass that provisions Envoy as a Deployment instead of the default DaemonSet looks like: + +```yaml +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: contour-with-envoy-deployment +spec: + controllerName: projectcontour.io/gateway-controller + parametersRef: + kind: ContourDeployment + group: projectcontour.io + name: contour-with-envoy-deployment-params + namespace: projectcontour +--- +kind: ContourDeployment +apiVersion: projectcontour.io/v1alpha1 +metadata: + namespace: projectcontour + name: contour-with-envoy-deployment-params +spec: + envoy: + workloadType: Deployment +``` + +All Gateways provisioned using the `contour-with-envoy-deployment` GatewayClass would get an Envoy Deployment. + +See [the API documentation][6] for all `ContourDeployment` options. + +It's important to note that, per the [GatewayClass spec][10]: + +> It is recommended that [GatewayClass] 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. + +Contour follows the recommended behavior, meaning changes to a GatewayClass and its parameters are not propagated down to existing Gateways. + +### Upgrades + +When the Contour Gateway Provisioner is upgraded to a new version, it will upgrade all Gateways it controls (both the control plane and the data plane). + +## Disabling Experimental Resources + +Some users may want to use Contour with the [Gateway API standard channel][4] instead of the experimental channel, to avoid installing alpha resources into their clusters. +To do this, Contour must be told to disable informers for the experimental resources. +In the Contour (control plane) deployment, use the `--disable-feature` flag for `contour serve` to disable informers for the experimental resources: + +```yaml +containers: +- name: contour + image: ghcr.io/projectcontour/contour: + command: ["contour"] + args: + - serve + - --incluster + - --xds-address=0.0.0.0 + - --xds-port=8001 + - --contour-cafile=/certs/ca.crt + - --contour-cert-file=/certs/tls.crt + - --contour-key-file=/certs/tls.key + - --config-path=/config/contour.yaml + - --disable-feature=tlsroutes + - --disable-feature=grpcroutes +``` + +[1]: https://gateway-api.sigs.k8s.io/ +[2]: https://gateway-api.sigs.k8s.io/concepts/conformance/#2-support-levels +[3]: https://gateway-api.sigs.k8s.io/guides/#install-experimental-channel +[4]: https://gateway-api.sigs.k8s.io/guides/#install-standard-channel +[5]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/#gatewayclass-parameters +[6]: https://projectcontour.io/docs/main/config/api/#projectcontour.io/v1alpha1.ContourDeployment +[7]: https://projectcontour.io/docs/main/config/api/#projectcontour.io/v1alpha1.GatewayConfig +[8]: https://gateway-api.sigs.k8s.io/api-types/gatewayclass/#gatewayclass-controller-selection +[9]: https://projectcontour.io/quickstart/contour-gateway-provisioner.yaml +[10]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.GatewayClass +[11]: https://gateway-api.sigs.k8s.io/concepts/api-overview/#route-resources +[12]: /docs/{{< param version >}}/guides/gateway-api +[13]: https://github.com/projectcontour/contour/issues/5970 +[14]: https://github.com/kubernetes-sigs/gateway-api/issues/2592 \ No newline at end of file diff --git a/site/content/docs/1.29/config/health-checks.md b/site/content/docs/1.29/config/health-checks.md new file mode 100644 index 00000000000..6dd1aac619d --- /dev/null +++ b/site/content/docs/1.29/config/health-checks.md @@ -0,0 +1,160 @@ +# Upstream Health Checks + +## HTTP Proxy Health Checking + +Active health checking can be configured on a per route basis. +Contour supports HTTP health checking and can be configured with various settings to tune the behavior. + +During HTTP health checking Envoy will send an HTTP request to the upstream Endpoints. +It expects a 200 response by default if the host is healthy (see `expectedStatuses` below for configuring the "healthy" status codes). +The upstream host can return 503 if it wants to immediately notify Envoy to no longer forward traffic to it. +It is important to note that these are health checks which Envoy implements and are separate from any other system such as those that exist in Kubernetes. + +```yaml +# httpproxy-health-checks.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: health-check + namespace: default +spec: + virtualhost: + fqdn: health.bar.com + routes: + - conditions: + - prefix: / + healthCheckPolicy: + path: /healthy + intervalSeconds: 5 + timeoutSeconds: 2 + unhealthyThresholdCount: 3 + healthyThresholdCount: 5 + services: + - name: s1-health + port: 80 + - name: s2-health + port: 80 +``` + +Health check configuration parameters: + +- `path`: HTTP endpoint used to perform health checks on upstream service (e.g. `/healthz`). It expects a 200 response if the host is healthy. The upstream host can return 503 if it wants to immediately notify downstream hosts to no longer forward traffic to it. +- `host`: The value of the host header in the HTTP health check request. If left empty (default value), the name "contour-envoy-healthcheck" will be used. +- `intervalSeconds`: The interval (seconds) between health checks. Defaults to 5 seconds if not set. +- `timeoutSeconds`: The time to wait (seconds) for a health check response. If the timeout is reached the health check attempt will be considered a failure. Defaults to 2 seconds if not set. +- `unhealthyThresholdCount`: The number of unhealthy health checks required before a host is marked unhealthy. Note that for http health checking if a host responds with 503 this threshold is ignored and the host is considered unhealthy immediately. Defaults to 3 if not defined. +- `healthyThresholdCount`: The number of healthy health checks required before a host is marked healthy. Note that during startup, only a single successful health check is required to mark a host healthy. +- `expectedStatuses`: An optional list of HTTP status ranges that are considered healthy. Ranges follow half-open semantics, meaning the start is inclusive and the end is exclusive. Statuses must be between 100 (inclusive) and 600 (exclusive). + +### Non-default expected statuses + +By default, only responses with a 200 status code will be considered healthy. +The set of response codes considered healthy can be customized by specifying ranges in `expectedStatuses`. +Ranges follow half-open semantics, meaning the start is inclusive and the end is exclusive. +Statuses must be between 100 (inclusive) and 600 (exclusive). +For example: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: health-check + namespace: default +spec: + virtualhost: + fqdn: health.bar.com + routes: + - conditions: + - prefix: / + healthCheckPolicy: + path: /healthy + intervalSeconds: 5 + timeoutSeconds: 2 + unhealthyThresholdCount: 3 + healthyThresholdCount: 5 + # Status codes 200 and 250-299 will be considered healthy. + expectedStatuses: + - start: 200 + end: 201 + - start: 250 + end: 300 + services: + - name: s1-health + port: 80 + - name: s2-health + port: 80 +``` + +Note that if `expectedStatuses` is specified, `200` must be explicitly included in one of the specified ranges if it is desired as a healthy status code. + +## TCP Proxy Health Checking + +Contour also supports TCP health checking and can be configured with various settings to tune the behavior. + +During TCP health checking Envoy will send a connect-only health check to the upstream Endpoints. +It is important to note that these are health checks which Envoy implements and are separate from any +other system such as those that exist in Kubernetes. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: tcp-health-check + namespace: default +spec: + virtualhost: + fqdn: health.bar.com + tcpproxy: + healthCheckPolicy: + intervalSeconds: 5 + timeoutSeconds: 2 + unhealthyThresholdCount: 3 + healthyThresholdCount: 5 + services: + - name: s1-health + port: 80 + - name: s2-health + port: 80 +``` + +TCP Health check policy configuration parameters: + +- `intervalSeconds`: The interval (seconds) between health checks. Defaults to 5 seconds if not set. +- `timeoutSeconds`: The time to wait (seconds) for a health check response. If the timeout is reached the health check attempt will be considered a failure. Defaults to 2 seconds if not set. +- `unhealthyThresholdCount`: The number of unhealthy health checks required before a host is marked unhealthy. Note that for http health checking if a host responds with 503 this threshold is ignored and the host is considered unhealthy immediately. Defaults to 3 if not defined. +- `healthyThresholdCount`: The number of healthy health checks required before a host is marked healthy. Note that during startup, only a single successful health check is required to mark a host healthy. + +## Specify the service health check port + +contour supports configuring an optional health check port for services. + +By default, the service's health check port is the same as the service's routing port. +If the service's health check port and routing port are different, you can configure the health check port separately. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: health-check + namespace: default +spec: + virtualhost: + fqdn: health.bar.com + routes: + - conditions: + - prefix: / + healthCheckPolicy: + path: /healthy + intervalSeconds: 5 + timeoutSeconds: 2 + unhealthyThresholdCount: 3 + healthyThresholdCount: 5 + services: + - name: s1-health + port: 80 + healthPort: 8998 + - name: s2-health + port: 80 +``` + +In this example, envoy will send a health check request to port `8998` of the `s1-health` service and port `80` of the `s2-health` service respectively . If the host is healthy, envoy will forward traffic to the `s1-health` service on port `80` and to the `s2-health` service on port `80`. diff --git a/site/content/docs/1.29/config/inclusion-delegation.md b/site/content/docs/1.29/config/inclusion-delegation.md new file mode 100644 index 00000000000..b9364ff1fcd --- /dev/null +++ b/site/content/docs/1.29/config/inclusion-delegation.md @@ -0,0 +1,139 @@ +# HTTPProxy Inclusion + +HTTPProxy permits the splitting of a system's configuration into separate HTTPProxy instances using **inclusion**. + +Inclusion, as the name implies, allows for one HTTPProxy object to be included in another, optionally with some conditions inherited from the parent. +Contour reads the inclusion tree and merges the included routes into one big object internally before rendering Envoy config. +Importantly, the included HTTPProxy objects do not have to be in the same namespace. + +Each tree of HTTPProxy starts with a root, the top level object of the configuration for a particular virtual host. +Each root HTTPProxy defines a `virtualhost` key, which describes properties such as the fully qualified name of the virtual host, TLS configuration, etc. + +HTTPProxies included from the root must not contain a virtualhost key. +Root objects cannot include other roots either transitively or directly. +This permits the owner of an HTTPProxy root to allow the inclusion of a portion of the route space inside a virtual host, and to allow that route space to be further subdivided with inclusions. +Because the path is not necessarily used as the only key, the route space can be multi-dimensional. + +## Conditions and Inclusion + +Like Routes, Inclusion may specify a set of [conditions][1]. +These conditions are added to any conditions on the routes included. +This process is recursive. + +Conditions are sets of individual condition statements, for example `prefix: /blog` is the condition that the matching request's path must start with `/blog`. +When conditions are combined through inclusion Contour merges the conditions inherited via inclusion with any conditions specified on the route. +This may result in duplicates, for example two `prefix:` conditions, mix of both `prefix:` and `exact` or `prefix` and `regex` conditions, or two header match conditions with the same name and value. +To resolve this Contour applies the following logic. + +- `prefix:` conditions are concatenated together in the order they were applied from the root object. For example the conditions, `prefix: /api`, `prefix: /v1` becomes a single `prefix: /api/v1` conditions. Note: Multiple prefixes cannot be supplied on a single set of Route conditions. +- `exact:` conditions are also concatenated just like `prefix:` conditions, but `exact:` conditions are not allowed in include match conditions. If the child httpproxy has `exact:` condition then after concatenation, it becomes a single `exact:` condition. For example, `prefix: /static` and `exact: /main.js` become a single `exact: /static/main.js` condition. +- `regex:` conditions are also concatenated just like `prefix:` conditions, but `regex:` conditions are not allowed in include match conditions. If the child httpproxy has `regex:` condition then after concatenation, it becomes a single `regex:` condition. For example, `prefix: /static` and `regex: /.*/main.js` become a single `regex: /static/.*/main.js` condition. +- Proxies with repeated identical `header:` conditions of type "exact match" (the same header keys exactly) are marked as "Invalid" since they create an un-routable configuration. + +## Configuring Inclusion + +Inclusion is a top-level field in the HTTPProxy [spec][2] element. +It requires one field, `name`, and has two optional fields: + +- `namespace`. This will assume the included HTTPProxy is in the same namespace if it's not specified. +- a `conditions` block. + +## Inclusion Within the Same Namespace + +HTTPProxies can include other HTTPProxy objects in the namespace by specifying the name of the object and its namespace in the top-level `includes` block. +Note that `includes` is a list, and so it must use the YAML list construct. + +In this example, the HTTPProxy `include-root` has included the configuration for paths matching `/service2` from the HTTPProxy named `service2` in the same namespace as `include-root` (the `default` namespace). +It's important to note that `service2` HTTPProxy has not defined a `virtualhost` property as it is NOT a root HTTPProxy. + +```yaml +# httpproxy-inclusion-samenamespace.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: include-root + namespace: default +spec: + virtualhost: + fqdn: root.bar.com + includes: + # Includes the /service2 path from service2 in the same namespace + - name: service2 + namespace: default + conditions: + - prefix: /service2 + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: service2 + namespace: default +spec: + routes: + - services: # matches /service2 + - name: s2 + port: 80 + - conditions: + - prefix: /blog # matches /service2/blog + services: + - name: blog + port: 80 +``` + +## Inclusion Across Namespaces + +Inclusion can also happen across Namespaces by specifying a `namespace` in the `inclusion`. +This is a particularly powerful paradigm for enabling multi-team Ingress management. + +If the `--watch-namespaces` configuration flag is used, it must define all namespaces that will be referenced by the inclusion. + +In this example, the root HTTPProxy has included configuration for paths matching `/blog` to the `blog` HTTPProxy object in the `marketing` namespace. + +```yaml +# httpproxy-inclusion-across-namespaces.yaml +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: namespace-include-root + namespace: default +spec: + virtualhost: + fqdn: ns-root.bar.com + includes: + # delegate the subpath, `/blog` to the HTTPProxy object in the marketing namespace with the name `blog` + - name: blog + namespace: marketing + conditions: + - prefix: /blog + routes: + - services: + - name: s1 + port: 80 + +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: blog + namespace: marketing +spec: + routes: + - services: + - name: s2 + port: 80 +``` + +## Orphaned HTTPProxy children + +It is possible for HTTPProxy objects to exist that have not been delegated to by another HTTPProxy. +These objects are considered "orphaned" and will be ignored by Contour in determining ingress configuration. + +[1]: request-routing#conditions +[2]: api/#projectcontour.io/v1.HTTPProxySpec diff --git a/site/content/docs/1.29/config/ingress.md b/site/content/docs/1.29/config/ingress.md new file mode 100644 index 00000000000..22e65bb0255 --- /dev/null +++ b/site/content/docs/1.29/config/ingress.md @@ -0,0 +1,94 @@ +# k8s Ingress Resource Support in Contour + + + + + +This document describes Contour's implementation of specific Ingress resource fields and features. +As the Ingress specification has evolved between v1beta1 and v1, any differences between versions are highlighted to ensure clarity for Contour users. + +**Note: As of Contour version 1.16.0, Contour is not compatible with Kubernetes versions that predate Ingress v1. This means Contour 1.16.0 and above require Kubernetes 1.19 and above. The Ingress v1beta1 resource is still available in Kubernetes 1.19 (but will be removed in 1.22) and the API server will convert such resources to Ingress v1 for Contour to subscribe to.** + +## Kubernetes Versions + +Contour is [validated against Kubernetes release versions N through N-2][1] (with N being the latest release). +For Kubernetes version 1.19+, the API server translates any Ingress v1beta1 resources to Ingress v1 and Contour watches Ingress v1 resources. + +## IngressClass and IngressClass Name + +In order to support differentiating between Ingress controllers or multiple instances of a single Ingress controller, users can create an [IngressClass resource][2] and specify an IngressClass name on a Ingress to reference it. +The IngressClass resource can be used to provide configuration to an Ingress controller watching resources it governs. +Contour supports watching an IngressClass resource specified with the `--ingress-class-name` flag to the `contour serve` command. +Contour does not require an IngressClass resource with the name passed in the aforementioned flag to exist, the name can just be used as an identifier for filtering which Ingress resources Contour reconciles into actual route configuration. + +Ingresses may specify an IngressClass name via the original annotation method or via the `ingressClassName` spec field. +As the `ingressClassName` field has been introduced on Ingress v1beta1, there should be no differences in IngressClass name filtering between the two available versions of the resource. +Contour uses its configured IngressClass name to filter Ingresses. +If the `--ingress-class-name` flag is provided, Contour will only accept Ingress resources that exactly match the specified IngressClass name via annotation or spec field, with the value in the annotation taking precedence. (The `--ingress-class-name` value can be a comma-separated list of class names to match against.) +If the flag is not passed to `contour serve` Contour will accept any Ingress resource that specifies the IngressClass name `contour` in annotation or spec fields or does not specify one at all. + +## Default Backend + +Contour supports the `defaultBackend` Ingress v1 spec field and equivalent `backend` v1beta1 version of the field. +See upstream [documentation][3] on this field. +Any requests that do not match an Ingress rule will be forwarded to this backend. +As TLS secrets on Ingresses are scoped to specific hosts, this default backend cannot serve TLS as it could match an unbounded set of hosts and configuring a matching set of TLS secrets would not be possible. +As is the case on Ingress rules, Contour only supports configuring a Service as a backend and does not support any other Kubernetes resource. + +## Ingress Rules + +See upstream [documentation][4] on Ingress rules. + +As with default backends, Contour only supports configuring a Service as a backend and does not support any other Kubernetes resource. + +Contour supports [wildcard hostnames][5] as documented by the upstream API as well as precise hostnames. +Wildcard hostnames are limited to the whole first DNS label of the hostname, e.g. `*.foo.com` is valid but `*foo.com`, `foo*.com`, `foo.*.com` are not. +`*` is also not a valid hostname. +Precise hostnames in Ingress or HTTPProxy configuration take higher precedence over wildcards. +For example, given an Ingress rule with the hostname `*.foo.com` routing to `service-a` and another Ingress rule or HTTPProxy route containing a subdomain (say `bar.foo.com`) routing to `service-b`, requests to `bar.foo.com` will be routed to `service-b`. +The Ingress admission controller validation ensures valid hostnames are present when creating an Ingress resource. + +Contour supports all of the various [path matching][6] types described by the Ingress spec. +Prior to Contour 1.14.0, path match types were ignored and path matching was performed with a Contour specific implementation. +Paths specified with any regex meta-characters (any of `^+*[]%`) were implemented as regex matches. +Any other paths were programmed in Envoy as "string prefix" matches. +This behavior is preserved in the `ImplementationSpecific` match type in Contour 1.14.0+ to ensure backwards compatibility. +`Exact` path matches will now result in matching requests to the given path exactly. +The `Prefix` patch match type will now result in matching requests with a "segment prefix" rather than a "string prefix" according to the spec (e.g. the prefix `/foo/bar` will match requests with paths `/foo/bar`, `/foo/bar/`, and `/foo/bar/baz`, but not `/foo/barbaz`). + +## TLS + +See upstream [documentation][7] on TLS configuration. + +A secret specified in an Ingress TLS element will only be applied to Ingress rules with `Host` configuration that exactly matches an element of the TLS `Hosts` field. +Any secrets that do not match an Ingress rule `Host` will be ignored. + +In Ingress v1beta1, the `secretName` field could contain a string with a full `namespace/name` identifier. +When used with Contour's [TLS certificate delegation][8], this allowed Ingresses to use a TLS certificate from a different namespace. +However, Ingress v1 does not allow the `secretName` field to contain a string with a full `namespace/name` identifier, because the field validation disallows the `/` character. +Instead, Ingress v1 resources can now use the `projectcontour.io/tls-cert-namespace` annotation, to define the namespace that contains the TLS certificate (if different than the Ingress's namespace). +This enables the TLS certificate delegation functionality to continue working for Ingress v1. +For more information and an example, see the [TLS certificate delegation documentation][8]. + +## Status + +In order to inform users of the address the Services their Ingress resources can be accessed at, Contour sets status on Ingress resources. +If `contour serve` is run with the `--ingress-status-address` flag, Contour will use the provided value to set the Ingress status address accordingly. +If not provided, Contour will use the address of the Envoy service using the passed in `--envoy-service-name` and `--envoy-service-namespace` flags. + +## Header Manipulation + +The Ingress resource does not allow adding or removing HTTP headers on requests or responses. +However, Contour does allow users to set a global HTTP header [policy configuration][9] which can be optionally applied to configuration generated from Ingress resources. +Contour enables this behavior with the `applyToIngress` boolean field (set to `true` to enable). + +[0]: https://github.com/kubernetes-sigs/ingress-controller-conformance +[1]: /resources/compatibility-matrix/ +[2]: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class +[3]: https://kubernetes.io/docs/concepts/services-networking/ingress/#default-backend +[4]: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-rules +[5]: https://kubernetes.io/docs/concepts/services-networking/ingress/#hostname-wildcards +[6]: https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types +[7]: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls +[8]: /docs/{{< param version >}}/config/tls-delegation/ +[9]: /docs/{{< param version >}}/configuration/#policy-configuration diff --git a/site/content/docs/1.29/config/ip-filtering.md b/site/content/docs/1.29/config/ip-filtering.md new file mode 100644 index 00000000000..161d39bc228 --- /dev/null +++ b/site/content/docs/1.29/config/ip-filtering.md @@ -0,0 +1,80 @@ +# IP Filtering + +Contour supports filtering requests based on the incoming ip address using Envoy's [RBAC Filter][1]. + +Requests can be either allowed or denied based on a CIDR range specified on the virtual host and/or individual routes. + +If the request's IP address is allowed, the request will be proxied to the appropriate upstream. +If the request's IP address is denied, an HTTP 403 (Forbidden) will be returned to the client. + +## Specifying Rules + +Rules are specified with the `ipAllowPolicy` and `ipDenyPolicy` fields on `virtualhost` and `route`: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: basic +spec: + virtualhost: + fqdn: foo-basic.bar.com + ipAllowPolicy: + # traffic is allowed if it came from localhost (i.e. co-located reverse proxy) + - cidr: 127.0.0.1/32 + source: Peer + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + # route-level ip filters override the virtualhost-level filters + ipAllowPolicy: + # traffic is allowed if it came from localhost (i.e. co-located reverse proxy) + - cidr: 127.0.0.1/32 + source: Peer + # and the request originated from an IP in this range + - cidr: 99.99.0.0/16 + source: Remote +``` + +### Specifying CIDR Ranges + +CIDR ranges may be ipv4 or ipv6. Bare IP addresses are interpreted as the CIDR range containing that one ip address only. + +Examples: +- `1.1.1.1/24` +- `127.0.0.1` +- `2001:db8::68/24` +- `2001:db8::68` + +### Allow vs Deny + +Filters are specified as either allow or deny: + +- `ipAllowPolicy` only allows requests that match the ip filters. +- `ipDenyPolicy` denies all requests unless they match the ip filters. + +Allow and deny policies cannot both be specified at the same time for a virtual host or route. + +### IP Source + +The `source` field controls how the ip address is selected from the request for filtering. + +- `source: Peer` filter rules will filter using Envoy's [direct_remote_ip][2], which is always the physical peer. +- `source: Remote` filter rules will filter using Envoy's [remote_ip][3], which may be inferred from the X-Forwarded-For header or proxy protocol. + +If using `source: Remote` with `X-Forwarded-For`, it may be necessary to configure Contour's `numTrustedHops` in [Network Parameters][4]. + +### Virtual Host and Route Filter Precedence + +IP filters on the virtual host apply to all routes included in the virtual host, unless the route specifies its own rules. + +Rules specified on a route override any rules defined on the virtual host, they are not additive. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/rbac_filter.html +[2]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#envoy-v3-api-field-config-rbac-v3-principal-direct-remote-ip +[3]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#envoy-v3-api-field-config-rbac-v3-principal-remote-ip +[4]: api/#projectcontour.io/v1.NetworkParameters + diff --git a/site/content/docs/1.29/config/jwt-verification.md b/site/content/docs/1.29/config/jwt-verification.md new file mode 100644 index 00000000000..3f884ad2aef --- /dev/null +++ b/site/content/docs/1.29/config/jwt-verification.md @@ -0,0 +1,182 @@ +# JWT Verification + +Contour supports verifying JSON Web Tokens (JWTs) on incoming requests, using Envoy's [jwt_authn HTTP filter][1]. +Specifically, the following properties can be checked: +- issuer field +- audiences field +- signature, using a configured JSON Web Key Store (JWKS) +- time restrictions (e.g. expiration, not before time) + +If verification succeeds, the request will be proxied to the appropriate upstream. +If verification fails, an HTTP 401 (Unauthorized) will be returned to the client. + +JWT verification is only supported on TLS-terminating virtual hosts. + +## Configuring providers and rules + +A JWT provider is configured for an HTTPProxy's virtual host, and defines how to verify JWTs: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: jwt-verification + namespace: default +spec: + virtualhost: + fqdn: example.com + tls: + secretName: example-com-tls-cert + jwtProviders: + - name: provider-1 + issuer: example.com + audiences: + - audience-1 + - audience-2 + remoteJWKS: + uri: https://example.com/jwks.json + timeout: 1s + cacheDuration: 5m + forwardJWT: true + routes: + ... +``` + +The provider above requires JWTs to have an issuer of example.com, an audience of either audience-1 or audience-2, and a signature that can be verified using the configured JWKS. +It also forwards the JWT to the backend via the `Authorization` header after successful verification. + +To apply a JWT provider as a requirement to a given route, specify a `jwtVerificationPolicy` for the route: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: jwt-verification + namespace: default +spec: + virtualhost: + fqdn: example.com + tls: + secretName: example-com-tls-cert + jwtProviders: + - name: provider-1 + ... + routes: + - conditions: + - prefix: / + jwtVerificationPolicy: + require: provider-1 + services: + - name: s1 + port: 80 + - conditions: + - prefix: /css + services: + - name: s1 + port: 80 +``` + +In the above example, the default route requires requests to carry JWTs that can be verified using provider-1. +The second route _excludes_ requests to paths starting with `/css` from JWT verification, because it does not have a JWT verification policy. + +### Configuring TLS validation for the JWKS server + +By default, the JWKS server's TLS certificate will not be validated, but validation can be requested by setting the `spec.virtualhost.jwtProviders[].remoteJWKS.validation` field. +This field has mandatory `caSecret` and `subjectName` fields, which specify the trusted root certificates with which to validate the server certificate and the expected server name. +The `caSecret` can be a namespaced name of the form `/`. +If the CA secret's namespace is not the same namespace as the `HTTPProxy` resource, [TLS Certificate Delegation][5] must be used to allow the owner of the CA certificate secret to delegate, for the purposes of referencing the CA certificate in a different namespace, permission to Contour to read the Secret object from another namespace. + +**Note:** If `spec.virtualhost.jwtProviders[].remoteJWKS.validation` is present, `spec.virtualhost.jwtProviders[].remoteJWKS.uri` must have a scheme of `https`. + +## Setting a default provider + +The previous section showed how to explicitly require JWT providers for specific routes. +An alternate approach is to define a JWT provider as the default by specifying `default: true` for it, in which case it is automatically applied to all routes unless they disable JWT verification. +The example from the previous section could alternately be configured as follows: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: jwt-verification + namespace: default +spec: + virtualhost: + fqdn: example.com + tls: + secretName: example-com-tls-cert + jwtProviders: + - name: provider-1 + default: true + ... + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + - conditions: + - prefix: /css + jwtVerificationPolicy: + disabled: true + services: + - name: s1 + port: 80 +``` + +In this case, the default route automatically has provider-1 applied, while the `/css` route explicitly disables JWT verification. + +One scenario where setting a default provider can be particularly useful is when using [HTTPProxy inclusion][2]. +Setting a default provider in the root HTTPProxy allows all routes in the child HTTPProxies to automatically have JWT verification applied. +For example: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: jwt-verification-root + namespace: default +spec: + virtualhost: + fqdn: example.com + tls: + secretName: example-com-tls-cert + jwtProviders: + - name: provider-1 + default: true + ... + includes: + - name: jwt-verification-child + namespace: default + conditions: + - prefix: /blog +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: jwt-verification-child + namespace: default +spec: + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 +``` + +In this case, all routes in the child HTTPProxy will automatically have JWT verification applied, without the owner of this HTTPProxy needing to configure it explicitly. + +## API documentation + +For more information on the HTTPProxy API for JWT verification, see: + +- [JWTProvider][3] +- [JWTVerificationPolicy][4] + + +[1]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter +[2]: /docs/{{< param version >}}/config/inclusion-delegation/ +[3]: /docs/{{< param version >}}/config/api/#projectcontour.io/v1.JWTProvider +[4]: /docs/{{< param version >}}/config/api/#projectcontour.io/v1.JWTVerificationPolicy +[5]: tls-delegation.md diff --git a/site/content/docs/1.29/config/overload-manager.md b/site/content/docs/1.29/config/overload-manager.md new file mode 100644 index 00000000000..33c96532eba --- /dev/null +++ b/site/content/docs/1.29/config/overload-manager.md @@ -0,0 +1,30 @@ +# Overload Manager + +Envoy uses heap memory when processing requests. +When the system runs out of memory or memory resource limit for the container is reached, Envoy process is terminated abruptly. +To avoid this, Envoy [overload manager][1] can be enabled. +Overload manager controls how much memory Envoy will allocate at maximum and what actions it takes when the limit is reached. + +Overload manager is disabled by default. +It can be enabled at deployment time by using `--overload-max-heap=[MAX_BYTES]` command line flag in [`contour bootstrap`][2] command. +The bootstrap command is executed in [init container of Envoy pod][3] to generate initial configuration for Envoy. +To enable overload manager, modify the deployment manifest and add for example `--overload-max-heap=2147483648` to set maximum heap size to 2 GiB. +The appropriate number of bytes can be different from system to system. + +After the feature is enabled, following two overload actions are configured to Envoy: + +* Shrink heap action is executed when 95% of the maximum heap size is reached. +* Envoy will stop accepting requests when 98% of the maximum heap size is reached. + +When requests are denied due to high memory pressure, `503 Service Unavailable` will be returned with a response body containing text `envoy overloaded`. +Shrink heap action will try to free unused heap memory, eventually allowing requests to be processed again. + +**NOTE:** +The side effect of overload is that Envoy will deny also requests `/ready` and `/stats` endpoints. +This is due to the way how Contour secures Envoy's admin API and exposes only selected admin API endpoints by proxying itself. +When readiness probe fails, the overloaded Envoy will be removed from the list of service endpoints. +If the maximum heap size is set too low, Envoy may be unable to free enough memory and never become ready again. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager +[2]: ../configuration#bootstrap-flags +[3]: https://github.com/projectcontour/contour/blob/cbec8eca9e8b639318588c5aa7ec0b5b751938c5/examples/render/contour.yaml#L5204-L5216 diff --git a/site/content/docs/1.29/config/rate-limiting.md b/site/content/docs/1.29/config/rate-limiting.md new file mode 100644 index 00000000000..7a69c22c079 --- /dev/null +++ b/site/content/docs/1.29/config/rate-limiting.md @@ -0,0 +1,366 @@ +# Rate Limiting + +- [Overview](#overview) +- [Local Rate Limiting](#local-rate-limiting) +- [Global Rate Limiting](#global-rate-limiting) + +## Overview + +Rate limiting is a means of protecting backend services against unwanted traffic. +This can be useful for a variety of different scenarios: + +- Protecting against denial-of-service (DoS) attacks by malicious actors +- Protecting against DoS incidents due to bugs in client applications/services +- Enforcing usage quotas for different classes of clients, e.g. free vs. paid tiers +- Controlling resource consumption/cost + +Envoy supports two forms of HTTP rate limiting: **local** and **global**. + +In local rate limiting, rate limits are enforced by each Envoy instance, without any communication with other Envoys or any external service. + +In global rate limiting, an external rate limit service (RLS) is queried by each Envoy via gRPC for rate limit decisions. + +Contour supports both forms of Envoy's rate limiting. + +## Local Rate Limiting + +The `HTTPProxy` API supports defining local rate limit policies that can be applied to either individual routes or entire virtual hosts. +Local rate limit policies define a maximum number of requests per unit of time that an Envoy should proxy to the upstream service. +Requests beyond the defined limit will receive a `429 (Too Many Requests)` response by default. +Local rate limit policies program Envoy's [HTTP local rate limit filter][1]. + +It's important to note that local rate limit policies apply *per Envoy pod*. +For example, a local rate limit policy of 100 requests per second for a given route will result in *each Envoy pod* allowing up to 100 requests per second for that route. + +### Defining a local rate limit + +Local rate limit policies can be defined for either routes or virtual hosts. A local rate limit policy requires a `requests` and a `units` field, defining the *number of requests per unit of time* that are allowed. `Requests` must be a positive integer, and `units` can be `second`, `minute`, or `hour`. Optionally, a `burst` parameter can also be provided, defining the number of requests above the baseline rate that are allowed in a short period of time. This would allow occasional larger bursts of traffic not to be rate limited. + +Local rate limiting for the virtual host: +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + namespace: default + name: ratelimited-vhost +spec: + virtualhost: + fqdn: local.projectcontour.io + rateLimitPolicy: + local: + requests: 100 + unit: hour + burst: 20 + routes: + - conditions: + - prefix: /s1 + services: + - name: s1 + port: 80 + - conditions: + - prefix: /s2 + services: + - name: s2 + port: 80 +``` + +Local rate limiting for the route: +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + namespace: default + name: ratelimited-route +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: /s1 + services: + - name: s1 + port: 80 + rateLimitPolicy: + local: + requests: 20 + unit: minute + - conditions: + - prefix: /s2 + services: + - name: s2 + port: 80 +``` + +### Customizing the response + +#### Response code + +By default, Envoy returns a `429 (Too Many Requests)` when a request is rate limited. +A non-default response code can optionally be configured as part of the local rate limit policy, in the `responseStatusCode` field. +The value must be in the 400-599 range. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + namespace: default + name: custom-ratelimit-response +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: /s1 + services: + - name: s1 + port: 80 + rateLimitPolicy: + local: + requests: 20 + unit: minute + responseStatusCode: 503 # Service Unavailable +``` + +#### Headers + +Headers can optionally be added to rate limited responses, by configuring the `responseHeadersToAdd` field. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + namespace: default + name: custom-ratelimit-response +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: /s1 + services: + - name: s1 + port: 80 + rateLimitPolicy: + local: + requests: 20 + unit: minute + responseHeadersToAdd: + - name: x-contour-ratelimited + value: "true" +``` + +## Global Rate Limiting + +The `HTTPProxy` API also supports defining global rate limit policies on routes and virtual hosts. + +In order to use global rate limiting, you must first select and deploy an external rate limit service (RLS). +There is an [Envoy rate limit service implementation][2], but any service that implements the [RateLimitService gRPC interface][3] is supported. + +### Configuring an external RLS with Contour + +Once you have deployed your RLS, you must configure it with Contour. + +Define an extension service for it (substituting values as appropriate): +```yaml +apiVersion: projectcontour.io/v1alpha1 +kind: ExtensionService +metadata: + namespace: projectcontour + name: ratelimit +spec: + protocol: h2 + services: + - name: ratelimit + port: 8081 +``` + +Now add a reference to it in the Contour config file: +```yaml +rateLimitService: + # The namespace/name of the extension service. + extensionService: projectcontour/ratelimit + # The domain value to pass to the RLS for all rate limit + # requests. Acts as a container for a set of rate limit + # definitions within the RLS. + domain: contour + # Whether to allow requests to proceed when the rate limit + # service fails to respond with a valid rate limit decision + # within the timeout defined on the extension service. + failOpen: true +``` + +### Defining a global rate limit policy + +Global rate limit policies can be defined for either routes or virtual hosts. Unlike local rate limit policies, global rate limit policies do not directly define a rate limit. Instead, they define a set of request descriptors that will be generated and sent to the external RLS for each request. The external RLS then makes the rate limit decision based on the descriptors and returns a response to Envoy. + +A global rate limit policy for the virtual host: +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + namespace: default + name: ratelimited-vhost +spec: + virtualhost: + fqdn: local.projectcontour.io + rateLimitPolicy: + global: + descriptors: + # the first descriptor has a single key-value pair: + # [ remote_address= ]. + - entries: + - remoteAddress: {} + # the second descriptor has two key-value pairs: + # [ remote_address=, vhost=local.projectcontour.io ]. + - entries: + - remoteAddress: {} + - genericKey: + key: vhost + value: local.projectcontour.io + routes: + - conditions: + - prefix: /s1 + services: + - name: s1 + port: 80 + - conditions: + - prefix: /s2 + services: + - name: s2 + port: 80 +``` + +A global rate limit policy for the route: +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + namespace: default + name: ratelimited-route +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: /s1 + services: + - name: s1 + port: 80 + rateLimitPolicy: + global: + descriptors: + # the first descriptor has a single key-value pair: + # [ remote_address= ]. + - entries: + - remoteAddress: {} + # the second descriptor has two key-value pairs: + # [ remote_address=, prefix=/s1 ]. + - entries: + - remoteAddress: {} + - genericKey: + key: prefix + value: /s1 + - conditions: + - prefix: /s2 + services: + - name: s2 + port: 80 +``` + +#### Descriptors & descriptor entries + +A descriptor is a list of key-value pairs, i.e. entries, that are generated for a request. The entries can be generated based on different criteria. If any entry in a descriptor cannot generate a key-value pair for a given request, then the entire descriptor is not generated (see the [Envoy documentation][8] for more information). When a global rate limit policy defines multiple descriptors, then *all* descriptors that can be generated will be generated and sent to the rate limit service for consideration. + +Below are the supported types of descriptor entries. + +##### GenericKey + +A `GenericKey` descriptor entry defines a static key-value pair. For example: + +```yaml +rateLimitPolicy: + global: + descriptors: + - entries: + - genericKey: + key: virtual-host-name + value: foo.bar.com +``` + +Produces a descriptor entry of `virtual-host-name=foo.bar.com`. + +The `key` field is optional and defaults to a value of `generic_key` if not specified. + +See the [Envoy documentation][4] for more information and examples. + +##### RemoteAddress + +A `RemoteAddress` descriptor entry has a key of `remote_address` and a value of the client IP address (using the trusted address from `x-forwarded-for`). For example: + +```yaml +rateLimitPolicy: + global: + descriptors: + - entries: + - remoteAddress: {} +``` + +Produces a descriptor entry of `remote_address=`. + +See the [Envoy documentation][5] for more information and examples. + +##### RequestHeader + +A `RequestHeader` descriptor entry has a static key and a value equal to the value of a specified header on the client request. If the header is not present, the descriptor entry is not generated. For example: + +```yaml +rateLimitPolicy: + global: + descriptors: + - entries: + - requestHeader: + headerName: My-Header + descriptorKey: my-header-value +``` + +Produces a descriptor entry of `my-header-value=`, for a client request that has the `My-Header` header. + +See the [Envoy documentation][6] for more information and examples. + +##### RequestHeaderValueMatch + +A `RequestHeaderValueMatch` descriptor entry has a key of `header_match` and a static value. The entry is only generated if the client request's headers match a specified set of criteria. For example: + +```yaml +rateLimitPolicy: + global: + descriptors: + - entries: + - requestHeaderValueMatch: + headers: + - name: My-Header + notpresent: true + - name: My-Other-Header + contains: contour + expectMatch: true + value: foo +``` + +Produces a descriptor entry of `header_match=foo`, for a client request that does not have the `My-Header` header, and does have the `My-Other-Header` header, with a value containing the substring "contour". + +Contour supports `present`, `notpresent`, `contains`, `notcontains`, `exact`, and `notexact` header match operators. + +The `expectMatch` field defaults to true if not specified. If true, the client request's headers must positively match the specified criteria in order for the descriptor entry to be generated. If false, the client request's header must *not* match the specified criteria in order for the descriptor entry to be generated. + +See the [Envoy documentation][7] for more information and examples. + + + +[1]: https://www.envoyproxy.io/docs/envoy/v1.17.0/configuration/http/http_filters/local_rate_limit_filter#config-http-filters-local-rate-limit +[2]: https://github.com/envoyproxy/ratelimit +[3]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ratelimit/v3/rls.proto +[4]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-msg-config-route-v3-ratelimit-action-generickey +[5]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-ratelimit-action-remoteaddress +[6]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-ratelimit-action-requestheaders +[7]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-ratelimit-action-headervaluematch +[8]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/rate_limit_filter#composing-actions diff --git a/site/content/docs/1.29/config/request-rewriting.md b/site/content/docs/1.29/config/request-rewriting.md new file mode 100644 index 00000000000..88fa3cc2508 --- /dev/null +++ b/site/content/docs/1.29/config/request-rewriting.md @@ -0,0 +1,337 @@ +# Request Rewriting + +## Path Rewriting + +HTTPProxy supports rewriting the HTTP request URL path prior to delivering the request to the backend service. +Rewriting is performed after a routing decision has been made, and never changes the request destination. + +The `pathRewritePolicy` field specifies how the path prefix should be rewritten. +The `replacePrefix` rewrite policy specifies a replacement string for a HTTP request path prefix match. +When this field is present, the path prefix that the request matched is replaced by the text specified in the `replacement` field. +If the HTTP request path is longer than the matched prefix, the remainder of the path is unchanged. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: rewrite-example + namespace: default +spec: + virtualhost: + fqdn: rewrite.bar.com + routes: + - services: + - name: s1 + port: 80 + pathRewritePolicy: + replacePrefix: + - replacement: /new/prefix +``` + +The `replacePrefix` field accepts an array of possible replacements. +When more than one `replacePrefix` array element is present, the `prefix` field can be used to disambiguate which replacement to apply. + +If no `prefix` field is present, the replacement is applied to all prefix matches made against the route. +If a `prefix` field is present, the replacement is applied only to routes that have an exactly matching prefix condition. +Specifying more than one `replacePrefix` entry is mainly useful when a HTTPProxy document is included into multiple parent documents. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: rewrite-example + namespace: default +spec: + virtualhost: + fqdn: rewrite.bar.com + routes: + - services: + - name: s1 + port: 80 + conditions: + - prefix: /v1/api + pathRewritePolicy: + replacePrefix: + - prefix: /v1/api + replacement: /app/api/v1 + - prefix: / + replacement: /app +``` + +## Header Rewriting + +HTTPProxy supports rewriting HTTP request and response headers. +The `Set` operation sets a HTTP header value, creating it if it doesn't already exist or overwriting it if it does. +The `Remove` operation removes a HTTP header. +The `requestHeadersPolicy` field is used to rewrite headers on a HTTP request, and the `responseHeadersPolicy` is used to rewrite headers on a HTTP response. +These fields can be specified on a route or on a specific service, depending on the rewrite granularity you need. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: header-rewrite-example +spec: + virtualhost: + fqdn: header.bar.com + routes: + - services: + - name: s1 + port: 80 + requestHeadersPolicy: + set: + - name: Host + value: external.dev + remove: + - Some-Header + - Some-Other-Header +``` + +Manipulating headers is also supported per-Service or per-Route. Headers can be set or +removed from the request or response as follows: + +per-Service: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: header-manipulation + namespace: default +spec: + virtualhost: + fqdn: headers.bar.com + routes: + - services: + - name: s1 + port: 80 + requestHeadersPolicy: + set: + - name: X-Foo + value: bar + remove: + - X-Baz + responseHeadersPolicy: + set: + - name: X-Service-Name + value: s1 + remove: + - X-Internal-Secret +``` + +per-Route: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: header-manipulation + namespace: default +spec: + virtualhost: + fqdn: headers.bar.com + routes: + - services: + - name: s1 + port: 80 + requestHeadersPolicy: + set: + - name: X-Foo + value: bar + remove: + - X-Baz + responseHeadersPolicy: + set: + - name: X-Service-Name + value: s1 + remove: + - X-Internal-Secret +``` + +In these examples we are setting the header `X-Foo` with value `baz` on requests +and stripping `X-Baz`. We are then setting `X-Service-Name` on the response with +value `s1`, and removing `X-Internal-Secret`. + +### Dynamic Header Values + +It is sometimes useful to set a header value using a dynamic value such as the +hostname where the Envoy Pod is running (`%HOSTNAME%`) or the subject of the +TLS client certificate (`%DOWNSTREAM_PEER_SUBJECT%`) or based on another header +(`%REQ(header)%`). + +Examples: +``` + requestHeadersPolicy: + set: + - name: X-Envoy-Hostname + value: "%HOSTNAME%" + - name: X-Host-Protocol + value: "%REQ(Host)% - %PROTOCOL%" + responseHeadersPolicy: + set: + - name: X-Envoy-Response-Flags + value: "%RESPONSE_FLAGS%" +``` + +Contour supports most of the custom request/response header variables offered +by Envoy - see the Envoy +documentation for details of what each of these resolve to: + +* `%DOWNSTREAM_REMOTE_ADDRESS%` +* `%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%` +* `%DOWNSTREAM_LOCAL_ADDRESS%` +* `%DOWNSTREAM_LOCAL_ADDRESS_WITHOUT_PORT%` +* `%DOWNSTREAM_LOCAL_PORT%` +* `%DOWNSTREAM_LOCAL_URI_SAN%` +* `%DOWNSTREAM_PEER_URI_SAN%` +* `%DOWNSTREAM_LOCAL_SUBJECT%` +* `%DOWNSTREAM_PEER_SUBJECT%` +* `%DOWNSTREAM_PEER_ISSUER%` +* `%DOWNSTREAM_TLS_SESSION_ID%` +* `%DOWNSTREAM_TLS_CIPHER%` +* `%DOWNSTREAM_TLS_VERSION%` +* `%DOWNSTREAM_PEER_FINGERPRINT_256%` +* `%DOWNSTREAM_PEER_FINGERPRINT_1%` +* `%DOWNSTREAM_PEER_SERIAL%` +* `%DOWNSTREAM_PEER_CERT%` +* `%DOWNSTREAM_PEER_CERT_V_START%` +* `%DOWNSTREAM_PEER_CERT_V_END%` +* `%HOSTNAME%` +* `%REQ(header-name)%` +* `%PROTOCOL%` +* `%RESPONSE_FLAGS%` +* `%RESPONSE_CODE_DETAILS%` +* `%UPSTREAM_REMOTE_ADDRESS%` + +Note that Envoy passes variables that can't be expanded through unchanged or +skips them entirely - for example: +* `%UPSTREAM_REMOTE_ADDRESS%` as a request header remains as + `%UPSTREAM_REMOTE_ADDRESS%` because as noted in the Envoy docs: "The upstream + remote address cannot be added to request headers as the upstream host has not + been selected when custom request headers are generated." +* `%DOWNSTREAM_TLS_VERSION%` is skipped if TLS is not in use +* Envoy ignores REQ headers that refer to an non-existent header - for example + `%REQ(Host)%` works as expected but `%REQ(Missing-Header)%` is skipped + +Contour already sets the `X-Request-Start` request header to +`t=%START_TIME(%s.%3f)%` which is the Unix epoch time when the request +started. + +To enable setting header values based on the destination service Contour also supports: + +* `%CONTOUR_NAMESPACE%` +* `%CONTOUR_SERVICE_NAME%` +* `%CONTOUR_SERVICE_PORT%` + +For example, with the following HTTPProxy object that has a per-Service requestHeadersPolicy using these variables: +``` +# httpproxy.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: basic + namespace: myns +spec: + virtualhost: + fqdn: foo-basic.bar.com + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + requestHeadersPolicy: + set: + - name: l5d-dst-override + value: "%CONTOUR_SERVICE_NAME%.%CONTOUR_NAMESPACE%.svc.cluster.local:%CONTOUR_SERVICE_PORT%" +``` +the values would be: +* `CONTOUR_NAMESPACE: "myns"` +* `CONTOUR_SERVICE_NAME: "s1"` +* `CONTOUR_SERVICE_PORT: "80"` + +and the `l5-dst-override` header would be set to `s1.myns.svc.cluster.local:80`. + +For per-Route requestHeadersPolicy only `%CONTOUR_NAMESPACE%` is set and using +`%CONTOUR_SERVICE_NAME%` and `%CONTOUR_SERVICE_PORT%` will end up as the +literal values `%%CONTOUR_SERVICE_NAME%%` and `%%CONTOUR_SERVICE_PORT%%`, +respectively. + +### Manipulating the Host header + +Contour allows users to manipulate the host header in two ways, using the `requestHeadersPolicy`. + +#### Static rewrite + +You can set the host to a static value. This can be done on the route and service level. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: static-host-header-rewrite-route +spec: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + - requestHeaderPolicy: + set: + - name: host + value: foo.com +``` + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: static-host-header-rewrite-service +spec: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + - requestHeaderPolicy: + set: + - name: host + value: "foo.com" +``` + +#### Dynamic rewrite + +You can also set the host header dynamically with the content of an existing header. +The format has to be `"%REQ()%"`. If the header is empty, it is ignored. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: dynamic-host-header-rewrite-route +spec: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + - requestHeaderPolicy: + set: + - name: host + value: "%REQ(x-rewrite-header)%" +``` + +Note: Only one of static or dynamic host rewrite can be specified. + +Note: Dynamic rewrite is only available at the route level and not possible on the service level. + +Note: Pay attention to the potential security implications of using this option, the provided header must come from a trusted source. + +Note: The header rewrite is only done while forwarding and has no bearing on the routing decision. diff --git a/site/content/docs/1.29/config/request-routing.md b/site/content/docs/1.29/config/request-routing.md new file mode 100644 index 00000000000..19ef5386e86 --- /dev/null +++ b/site/content/docs/1.29/config/request-routing.md @@ -0,0 +1,535 @@ +# Request Routing + +A HTTPProxy object must have at least one route or include defined. +In this example, any requests to `multi-path.bar.com/blog` or `multi-path.bar.com/blog/*` will be routed to the Service `s2` using the prefix conditions. Requests to `multi-path.bar.com/feed` will be routed to Service `s2` using exact match condition. +All other requests to the host `multi-path.bar.com` will be routed to the Service `s1`. + +```yaml +# httpproxy-multiple-paths.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: multiple-paths + namespace: default +spec: + virtualhost: + fqdn: multi-path.bar.com + routes: + - conditions: + - prefix: / # matches everything else + services: + - name: s1 + port: 80 + - conditions: + - prefix: /blog # matches `multi-path.bar.com/blog` or `multi-path.bar.com/blog/*` + services: + - name: s2 + port: 80 + - conditions: + - exact: /feed # matches `multi-path.bar.com/feed` only + services: + - name: s2 + port: 80 +``` + +In the following example, we match on headers and query parameters and send to different services, with a default route if those do not match. + +```yaml +# httpproxy-multiple-headers.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: multiple-paths + namespace: default +spec: + virtualhost: + fqdn: multi-path.bar.com + routes: + - conditions: + - header: + name: x-os + contains: ios + services: + - name: s1 + port: 80 + - conditions: + - header: + name: x-os + contains: android + services: + - name: s2 + port: 80 + - conditions: + - queryParameter: + name: os + exact: other + ignoreCase: true + services: + - name: s3 + port: 80 + - services: + - name: s4 + port: 80 +``` + +## Conditions + +Each Route entry in a HTTPProxy **may** contain one or more conditions. +These conditions are combined with an AND operator on the route passed to Envoy. +Conditions can be either a `prefix`, `exact`, `regex`, `header` or a `queryParameter` condition. At most one of `prefix`, `exact` or `regex` can be used in one condition block. + +#### Prefix conditions + +Paths defined are matched using prefix conditions. +Up to one prefix condition may be present in any condition block. + +Prefix conditions **must** start with a `/` if they are present. + +#### Exact conditions + +Paths defined are matched using exact conditions. +Up to one exact condition may be present in any condition block. Any condition block can +either have a regex condition, exact condition or prefix condition, but not multiple together. Exact conditions are +only allowed in route match conditions and not in include match conditions. + +Exact conditions **must** start with a `/` if they are present. + +#### Regex conditions + +Paths defined are matched using regex expressions. +Up to one regex condition may be present in any condition block. Any condition block can +either have a regex condition, exact condition or prefix condition, but not multiple together. Regex conditions are +only allowed in route match conditions and not in include match conditions. + +Regex conditions **must** start with a `/` if they are present. + +#### Header conditions + +For `header` conditions there is the following structure: + +1. one required field, `name` +2. six operator fields: `present`, `notpresent`, `contains`, `notcontains`, `exact`, and `notexact` +3. two optional modifiers: `ignoreCase` and `treatMissingAsEmpty` + +Operators: +- `present` is a boolean and checks that the header is present. The value will not be checked. + +- `notpresent` similarly checks that the header is *not* present. + +- `contains` is a string, and checks that the header contains the string. `notcontains` similarly checks that the header does *not* contain the string. + +- `exact` is a string, and checks that the header exactly matches the whole string. `notexact` checks that the header does *not* exactly match the whole string. + +- `regex` is a string representing a regular expression, and checks that the header value matches against the given regular expression. + +Modifiers: +- `ignoreCase`: IgnoreCase specifies that string matching should be case insensitive. It has no effect on the `Regex` parameter. +- `treatMissingAsEmpty`: specifies if the header match rule specified header does not exist, this header value will be treated as empty. Defaults to false. Unlike the underlying Envoy implementation this is **only** supported for negative matches (e.g. NotContains, NotExact). + +#### Query parameter conditions + +Similar to the `header` conditions, `queryParameter` conditions also require the +`name` field to be specified, which represents the name of the query parameter +e.g. `search` when the query string looks like `/?search=term` and `term` +representing the value. + +There are six operator fields: `exact`, `prefix`, `suffix`, `regex`, `contains` +and `present` and a modifier `ignoreCase` which can be used together with all of +the operator fields except `regex` and `present`. + +- `exact` is a string, and checks that the query parameter value exactly matches + the whole string. + +- `prefix` is a string, and checks that the query parameter value is prefixed by + the given value. + +- `suffix` is a string, and checks that the query parameter value is suffixed by + the given value. + +- `regex` is a string representing a regular expression, and checks that the + query parameter value matches against the given regular expression. + +- `contains` is a string, and checks that the query parameter value contains + the given string. + +- `present` is a boolean, and checks that the query parameter is present. The + value will not be checked. + +- `ignoreCase` is a boolean, and if set to `true` it will enable case + insensitive matching for any of the string operator matching methods. + +## Request Redirection + +HTTP redirects can be implemented in HTTPProxy using `requestRedirectPolicy` on a route. +In the following basic example, requests to `example.com` are redirected to `www.example.com`. +We configure a root HTTPProxy for `example.com` that contains redirect configuration. +We also configure a root HTTPProxy for `www.example.com` that represents the destination of the redirect. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: example-com +spec: + virtualhost: + fqdn: example.com + routes: + - conditions: + - prefix: / + requestRedirectPolicy: + hostname: www.example.com +``` + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: www-example-com +spec: + virtualhost: + fqdn: www.example.com + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 +``` + +In addition to specifying the hostname to set in the `location` header, the scheme, port, and returned status code of the redirect response can be configured. +Configuration of the path or a path prefix replacement to modify the path of the returned `location` can be included as well. +See [the API specification][3] for more detail. + +## Multiple Upstreams + +One of the key HTTPProxy features is the ability to support multiple services for a given path: + +```yaml +# httpproxy-multiple-upstreams.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: multiple-upstreams + namespace: default +spec: + virtualhost: + fqdn: multi.bar.com + routes: + - services: + - name: s1 + port: 80 + - name: s2 + port: 80 +``` + +In this example, requests for `multi.bar.com/` will be load balanced across two Kubernetes Services, `s1`, and `s2`. +This is helpful when you need to split traffic for a given URL across two different versions of an application. + +### Upstream Weighting + +Building on multiple upstreams is the ability to define relative weights for upstream Services. +This is commonly used for canary testing of new versions of an application when you want to send a small fraction of traffic to a specific Service. + +```yaml +# httpproxy-weight-shifting.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: weight-shifting + namespace: default +spec: + virtualhost: + fqdn: weights.bar.com + routes: + - services: + - name: s1 + port: 80 + weight: 10 + - name: s2 + port: 80 + weight: 90 +``` + +In this example, we are sending 10% of the traffic to Service `s1`, while Service `s2` receives the remaining 90% of traffic. + +HTTPProxy weighting follows some specific rules: + +- If no weights are specified for a given route, it's assumed even distribution across the Services. +- Weights are relative and do not need to add up to 100. If all weights for a route are specified, then the "total" weight is the sum of those specified. As an example, if weights are 20, 30, 20 for three upstreams, the total weight would be 70. In this example, a weight of 30 would receive approximately 42.9% of traffic (30/70 = .4285). +- If some weights are specified but others are not, then it's assumed that upstreams without weights have an implicit weight of zero, and thus will not receive traffic. + +### Traffic mirroring + +Per route, a service can be nominated as a mirror. +The mirror service will receive a copy of the read traffic sent to any non mirror service. +The mirror traffic is considered _read only_, any response by the mirror will be discarded. + +This service can be useful for recording traffic for later replay or for smoke testing new deployments. + +`weight` can be optionally set (in the space of integers 1-100) to mirror the corresponding percent of traffic (ie. `weight: 5` mirrors 5% of traffic). Omitting the `weight` field results in 100% traffic mirroring. There is unexpected behavior if `weight` is explicitly set to 0, 100% traffic will be mirrored. This occurs because we cannot distinguish undefined variables from explicitly setting them to default values, and omission of a `weight` must mirror full traffic. +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: traffic-mirror + namespace: default +spec: + virtualhost: + fqdn: www.example.com + routes: + - conditions: + - prefix: / + services: + - name: www + port: 80 + - name: www-mirror + port: 80 + mirror: true +``` + +## Response Timeouts + +Each Route can be configured to have a timeout policy and a retry policy as shown: + +```yaml +# httpproxy-response-timeout.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: response-timeout + namespace: default +spec: + virtualhost: + fqdn: timeout.bar.com + routes: + - timeoutPolicy: + response: 1s + idle: 10s + idleConnection: 60s + retryPolicy: + count: 3 + perTryTimeout: 150ms + services: + - name: s1 + port: 80 +``` + +In this example, requests to `timeout.bar.com/` will have a response timeout policy of 1s. +This refers to the time that spans between the point at which complete client request has been processed by the proxy, and when the response from the server has been completely processed. + +- `timeoutPolicy.response` Timeout for receiving a response from the server after processing a request from client. +If not supplied, Envoy's default value of 15s applies. +More information can be found in [Envoy's documentation][4]. +- `timeoutPolicy.idle` Timeout for how long the proxy should wait while there is no activity during single request/response (for HTTP/1.1) or stream (for HTTP/2). +Timeout will not trigger while HTTP/1.1 connection is idle between two consecutive requests. +If not specified, there is no per-route idle timeout, though a connection manager-wide stream idle timeout default of 5m still applies. +More information can be found in [Envoy's documentation][6]. +- `timeoutPolicy.idleConnection` Timeout for how long connection from the proxy to the upstream service is kept when there are no active requests. +If not supplied, Envoy’s default value of 1h applies. +More information can be found in [Envoy's documentation][8]. + +TimeoutPolicy durations are expressed in the Go [Duration format][5]. +Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +The string "infinity" is also a valid input and specifies no timeout. +A value of "0s" will be treated as if the field were not set, i.e. by using Envoy's default behavior. +Example input values: "300ms", "5s", "1m". + +- `retryPolicy`: A retry will be attempted if the server returns an error code in the 5xx range, or if the server takes more than `retryPolicy.perTryTimeout` to process a request. + +- `retryPolicy.count` specifies the maximum number of retries allowed. This parameter is optional and defaults to 1. Set to -1 to disable. If set to 0, the Envoy default of 1 is used. + +- `retryPolicy.perTryTimeout` specifies the timeout per retry. If this field is greater than the request timeout, it is ignored. This parameter is optional. + If left unspecified, `timeoutPolicy.request` will be used. + +## Load Balancing Strategy + +Each route can have a load balancing strategy applied to determine which of its Endpoints is selected for the request. +The following list are the options available to choose from: + +- `RoundRobin`: Each healthy upstream Endpoint is selected in round-robin order (Default strategy if none selected). +- `WeightedLeastRequest`: The least request load balancer uses different algorithms depending on whether hosts have the same or different weights in an attempt to route traffic based upon the number of active requests or the load at the time of selection. +- `Random`: The random strategy selects a random healthy Endpoints. +- `RequestHash`: The request hashing strategy allows for load balancing based on request attributes. An upstream Endpoint is selected based on the hash of an element of a request. For example, requests that contain a consistent value in an HTTP request header will be routed to the same upstream Endpoint. Currently, only hashing of HTTP request headers, query parameters and the source IP of a request is supported. +- `Cookie`: The cookie load balancing strategy is similar to the request hash strategy and is a convenience feature to implement session affinity, as described below. + +More information on the load balancing strategy can be found in [Envoy's documentation][7]. + +The following example defines the strategy for the route `/` as `WeightedLeastRequest`. + +```yaml +# httpproxy-lb-strategy.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: lb-strategy + namespace: default +spec: + virtualhost: + fqdn: strategy.bar.com + routes: + - conditions: + - prefix: / + services: + - name: s1-strategy + port: 80 + - name: s2-strategy + port: 80 + loadBalancerPolicy: + strategy: WeightedLeastRequest +``` + +The below example demonstrates how request hash load balancing policies can be configured: + +Request hash headers +```yaml +# httpproxy-lb-request-hash.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: lb-request-hash + namespace: default +spec: + virtualhost: + fqdn: request-hash.bar.com + routes: + - conditions: + - prefix: / + services: + - name: httpbin + port: 8080 + loadBalancerPolicy: + strategy: RequestHash + requestHashPolicies: + - headerHashOptions: + headerName: X-Some-Header + terminal: true + - headerHashOptions: + headerName: User-Agent + - hashSourceIP: true +``` +In this example, if a client request contains the `X-Some-Header` header, the value of the header will be hashed and used to route to an upstream Endpoint. This could be used to implement a similar workflow to cookie-based session affinity by passing a consistent value for this header. If it is present, because it is set as a `terminal` hash option, Envoy will not continue on to process to `User-Agent` header or source IP to calculate a hash. If `X-Some-Header` is not present, Envoy will use the `User-Agent` header value to make a routing decision along with the source IP of the client making the request. These policies can be used alone or as shown for an advanced routing decision. + + +Request hash source ip +```yaml +# httpproxy-lb-request-hash-ip.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: lb-request-hash + namespace: default +spec: + virtualhost: + fqdn: request-hash.bar.com + routes: + - conditions: + - prefix: / + services: + - name: httpbin + port: 8080 + loadBalancerPolicy: + strategy: RequestHash + requestHashPolicies: + - hashSourceIP: true +``` + +Request hash query parameters +```yaml +# httpproxy-lb-request-hash.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: lb-request-hash + namespace: default +spec: + virtualhost: + fqdn: request-hash.bar.com + routes: + - conditions: + - prefix: / + services: + - name: httpbin + port: 8080 + loadBalancerPolicy: + strategy: RequestHash + requestHashPolicies: + - queryParameterHashOptions: + prameterName: param1 + terminal: true + - queryParameterHashOptions: + parameterName: param2 +``` + +## Session Affinity + +Session affinity, also known as _sticky sessions_, is a load balancing strategy whereby a sequence of requests from a single client are consistently routed to the same application backend. +Contour supports session affinity on a per-route basis with `loadBalancerPolicy` `strategy: Cookie`. + +```yaml +# httpproxy-sticky-sessions.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: httpbin + namespace: default +spec: + virtualhost: + fqdn: httpbin.davecheney.com + routes: + - services: + - name: httpbin + port: 8080 + loadBalancerPolicy: + strategy: Cookie +``` + +Session affinity is based on the premise that the backend servers are robust, do not change ordering, or grow and shrink according to load. +None of these properties are guaranteed by a Kubernetes cluster and will be visible to applications that rely heavily on session affinity. + +Any perturbation in the set of pods backing a service risks redistributing backends around the hash ring. + +## Internal Redirects + +HTTPProxy supports handling 3xx redirects internally, that is capturing a configurable 3xx redirect response, synthesizing a new request, sending it to the upstream specified by the new route match, and returning the redirected response as the response to the original request. + +Internal redirects can be enabled in HTTPProxy by defining an `internalRedirectPolicy` on a route. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: myservice + namespace: prod +spec: + virtualhost: + fqdn: foo.com + routes: + - conditions: + - prefix: /download + services: + - name: foo + port: 8080 + internalRedirectPolicy: + maxInternalRedirects: 5 + redirectResponseCodes: [ 302 ] + allowCrossSchemeRedirect: SafeOnly + denyRepeatedRouteRedirect: true +``` + +In this example, a sample redirect flow might look like this: + +1. Client sends a `GET` request for http://foo.com/download. +2. Upstream `foo` returns a `302` response with `location: http://foo.com/myfile`. +3. Envoy lookups a route for http://foo.com/myfile and sends a new `GET` request to the corresponding upstream with the additional request header `x-envoy-original-url: http://foo.com/download`. +4. Envoy proxies the response data for http://foo.com/myfile to the client as the response to the original request. + +See [the API specification][9] and [Envoy's documentation][10] for more detail. + +[3]: /docs/{{< param version >}}/config/api/#projectcontour.io/v1.HTTPRequestRedirectPolicy +[4]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-timeout +[5]: https://godoc.org/time#ParseDuration +[6]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-idle-timeout +[7]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/overview +[8]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-idle-timeout +[9] /docs/{{< param version >}}/config/api/#projectcontour.io/v1.HTTPInternalRedirectPolicy +[10] https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/http/http_connection_management.html#internal-redirects diff --git a/site/content/docs/1.29/config/slow-start.md b/site/content/docs/1.29/config/slow-start.md new file mode 100644 index 00000000000..b44cc18fdc3 --- /dev/null +++ b/site/content/docs/1.29/config/slow-start.md @@ -0,0 +1,39 @@ +# Slow Start Mode + +Slow start mode is a configuration setting that is used to gradually increase the amount of traffic targeted to a newly added upstream endpoint. +By default, the amount of traffic will increase linearly for the duration of time window set by `window` field, starting from 10% of the target load balancing weight and increasing to 100% gradually. +The easing function for the traffic increase can be adjusted by setting optional field `aggression`. +A value above 1.0 results in a more aggressive increase initially, slowing down when nearing the end of the time window. +Value below 1.0 results in slow initial increase, picking up speed when nearing the end of the time window. +Optional field `minWeightPercent` can be set to change the minimum percent of target weight. +It is used to avoid too small new weight, which may cause endpoint to receive no traffic in beginning of the slow start window. + +Slow start mode can be useful for example with JVM based applications, that might otherwise get overwhelmed during JIT warm-up period. +Such applications may respond to requests slowly or return errors immediately after pod start or after container restarts. +User impact of this behavior can be mitigated by using slow start configuration to gradually increase traffic to recently started service endpoints. + +The following example configures slow start mode for a service: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: slow-start +spec: + virtualhost: + fqdn: www.example.com + routes: + - services: + - name: java-app + port: 80 + slowStartPolicy: + window: 3s + aggression: "1.0" + minWeightPercent: 10 +``` + +Slow start mode works only with `RoundRobin` and `WeightedLeastRequest` [load balancing strategies][2]. +For more details see [Envoy documentation][1]. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/slow_start +[2]: api/#projectcontour.io/v1.LoadBalancerPolicy diff --git a/site/content/docs/1.29/config/tls-delegation.md b/site/content/docs/1.29/config/tls-delegation.md new file mode 100644 index 00000000000..155796fe7eb --- /dev/null +++ b/site/content/docs/1.29/config/tls-delegation.md @@ -0,0 +1,79 @@ +# TLS Certificate Delegation + +In order to support wildcard certificates, TLS certificates for a `*.somedomain.com`, which are stored in a namespace controlled by the cluster administrator, Contour supports a facility known as TLS Certificate Delegation. +This facility allows the owner of a TLS certificate to delegate, for the purposes of referencing the TLS certificate, permission to Contour to read the Secret object from another namespace. +Delegation works for both HTTPProxy and Ingress resources, however it needs an annotation to work with Ingress v1. + +If the `--watch-namespaces` configuration flag is used, it must define all namespaces that will be referenced by the delegation. + +The [`TLSCertificateDelegation`][1] resource defines a set of `delegations` in the `spec`. +Each delegation references a `secretName` from the namespace where the `TLSCertificateDelegation` is created as well as describing a set of `targetNamespaces` in which the certificate can be referenced. +If all namespaces should be able to reference the secret, then set `"*"` as the value of `targetNamespaces` (see example below). + +```yaml +apiVersion: projectcontour.io/v1 +kind: TLSCertificateDelegation +metadata: + name: example-com-wildcard + namespace: www-admin +spec: + delegations: + - secretName: example-com-wildcard + targetNamespaces: + - example-com + - secretName: another-com-wildcard + targetNamespaces: + - "*" +``` + +In this example, the permission for Contour to reference the Secret `example-com-wildcard` in the `www-admin` namespace has been delegated to HTTPProxy and Ingress objects in the `example-com` namespace. +Also, the permission for Contour to reference the Secret `another-com-wildcard` from all namespaces has been delegated to all HTTPProxy and Ingress objects in the cluster. + +To reference the secret from an HTTPProxy or Ingress v1beta1 you must use the slash syntax in the `secretName`: +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: www + namespace: example-com +spec: + virtualhost: + fqdn: foo2.bar.com + tls: + secretName: www-admin/example-com-wildcard + routes: + - services: + - name: s1 + port: 80 +``` + +To reference the secret from an Ingress v1 you must use the `projectcontour.io/tls-cert-namespace` annotation: +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + projectcontour.io/tls-cert-namespace: www-admin + name: www + namespace: example-com +spec: + rules: + - host: foo2.bar.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: s1 + port: + number: 80 + tls: + - hosts: + - foo2.bar.com + secretName: example-com-wildcard +``` + + +[0]: https://github.com/projectcontour/contour/issues/3544 +[1]: /docs/{{< param version >}}/config/api/#projectcontour.io/v1.TLSCertificateDelegation diff --git a/site/content/docs/1.29/config/tls-termination.md b/site/content/docs/1.29/config/tls-termination.md new file mode 100644 index 00000000000..d1b26dc2f4e --- /dev/null +++ b/site/content/docs/1.29/config/tls-termination.md @@ -0,0 +1,353 @@ +# TLS Termination + +HTTPProxy follows a similar pattern to Ingress for configuring TLS credentials. + +You can secure a HTTPProxy by specifying a Secret that contains TLS private key and certificate information. +If multiple HTTPProxies utilize the same Secret, the certificate must include the necessary Subject Authority Name (SAN) for each fqdn. + +Contour (via Envoy) requires that clients send the Server Name Indication (SNI) TLS extension so that requests can be routed to the correct virtual host. +Virtual hosts are strongly bound to SNI names. +This means that the Host header in HTTP requests must match the SNI name that was sent at the start of the TLS session. + +Contour also follows a "secure first" approach. +When TLS is enabled for a virtual host, any request to the insecure port is redirected to the secure interface with a 301 redirect. +Specific routes can be configured to override this behavior and handle insecure requests by enabling the `spec.routes.permitInsecure` parameter on a Route. + +The TLS secret must: +- be a Secret of type `kubernetes.io/tls`. This means that it must contain keys named `tls.crt` and `tls.key` that contain the certificate and private key to use for TLS, in PEM format. + +The TLS secret may also: +- add any chain CA certificates required for validation into the `tls.crt` PEM bundle. If this is the case, the serving certificate must be the first certificate in the bundle and the intermediate CA certificates must be appended in issuing order. + +```yaml +# ingress-tls.secret.yaml +apiVersion: v1 +data: + tls.crt: base64 encoded cert + tls.key: base64 encoded key +kind: Secret +metadata: + name: testsecret + namespace: default +type: kubernetes.io/tls +``` + +The HTTPProxy can be configured to use this secret using `tls.secretName` property: + +```yaml +# httpproxy-tls.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: tls-example + namespace: default +spec: + virtualhost: + fqdn: foo2.bar.com + tls: + secretName: testsecret + routes: + - services: + - name: s1 + port: 80 +``` + +If the `tls.secretName` property contains a slash, eg. `somenamespace/somesecret` then, subject to TLS Certificate Delegation, the TLS certificate will be read from `somesecret` in `somenamespace`. +See TLS Certificate Delegation below for more information. + +The TLS **Minimum Protocol Version** a virtual host should negotiate can be specified by setting the `spec.virtualhost.tls.minimumProtocolVersion`: + +- 1.3 +- 1.2 (Default) + +## Fallback Certificate + +Contour provides virtual host based routing, so that any TLS request is routed to the appropriate service based on both the server name requested by the TLS client and the HOST header in the HTTP request. + +Since the HOST Header is encrypted during TLS handshake, it can’t be used for virtual host based routing unless the client sends HTTPS requests specifying hostname using the TLS server name, or the request is first decrypted using a default TLS certificate. + +Some legacy TLS clients do not send the server name, so Envoy does not know how to select the right certificate. A fallback certificate is needed for these clients. + +_**Note:** +The minimum TLS protocol version for any fallback request is defined by the `minimum TLS protocol version` set in the Contour configuration file. +Enabling the fallback certificate is not compatible with TLS client authentication._ + +### Fallback Certificate Configuration + +First define the `namespace/name` in the [Contour configuration file][1] of a Kubernetes secret which will be used as the fallback certificate. +Any HTTPProxy which enables fallback certificate delegation must have the fallback certificate delegated to the namespace in which the HTTPProxy object resides. + +To do that, configure `TLSCertificateDelegation` to delegate the fallback certificate to specific or all namespaces (e.g. `*`) which should be allowed to enable the fallback certificate. +Finally, for each root HTTPProxy, set the `Spec.TLS.enableFallbackCertificate` parameter to allow that HTTPProxy to opt-in to the fallback certificate routing. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: fallback-tls-example + namespace: defaultub +spec: + virtualhost: + fqdn: fallback.bar.com + tls: + secretName: testsecret + enableFallbackCertificate: true + routes: + - services: + - name: s1 + port: 80 +--- +apiVersion: projectcontour.io/v1 +kind: TLSCertificateDelegation +metadata: + name: fallback-delegation + namespace: www-admin +spec: + delegations: + - secretName: fallback-secret-name + targetNamespaces: + - "*" +``` + +## Permitting Insecure Requests + +A HTTPProxy can be configured to permit insecure requests to specific Routes. +In this example, any request to `foo2.bar.com/blog` will not receive a 301 redirect to HTTPS, but the `/` route will: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: tls-example-insecure + namespace: default +spec: + virtualhost: + fqdn: foo2.bar.com + tls: + secretName: testsecret + routes: + - services: + - name: s1 + port: 80 + - conditions: + - prefix: /blog + permitInsecure: true + services: + - name: s2 + port: 80 +``` + +## Client Certificate Validation + +It is possible to protect the backend service from unauthorized external clients by requiring the client to present a valid TLS certificate. +Envoy will validate the client certificate by verifying that it is not expired and that a chain of trust can be established to the configured trusted root CA certificate. +Only those requests with a valid client certificate will be accepted and forwarded to the backend service. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: with-client-auth +spec: + virtualhost: + fqdn: www.example.com + tls: + secretName: secret + clientValidation: + caSecret: client-root-ca + routes: + - services: + - name: s1 + port: 80 +``` + +The preceding example enables validation by setting the optional `clientValidation` attribute. +Its mandatory attribute `caSecret` contains a name of an existing Kubernetes Secret that must be of type "Opaque" and have only a data key named `ca.crt`. +The data value of the key `ca.crt` must be a PEM-encoded certificate bundle and it must contain all the trusted CA certificates that are to be used for validating the client certificate. +If the Opaque Secret also contains one of either `tls.crt` or `tls.key` keys, it will be ignored. + +By default, client certificates are required but some applications might support different authentication schemes. In that case you can set the `optionalClientCertificate` field to `true`. A client certificate will be requested, but the connection is allowed to continue if the client does not provide one. If a client certificate is sent, it will be verified according to the other properties, which includes disabling validations if `skipClientCertValidation` is set. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: with-optional-client-auth +spec: + virtualhost: + fqdn: www.example.com + tls: + secretName: secret + clientValidation: + caSecret: client-root-ca + optionalClientCertificate: true + routes: + - services: + - name: s1 + port: 80 +``` + +When using external authorization, it may be desirable to use an external authorization server to validate client certificates on requests, rather than the Envoy proxy. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: with-client-auth-and-ext-authz +spec: + virtualhost: + fqdn: www.example.com + authorization: + # external authorization server configuration + tls: + secretName: secret + clientValidation: + caSecret: client-root-ca + skipClientCertValidation: true + routes: + - services: + - name: s1 + port: 80 +``` + +In the above example, setting the `skipClientCertValidation` field to `true` will configure Envoy to require client certificates on requests and pass them along to a configured authorization server. +Failed validation of client certificates by Envoy will be ignored and the `fail_verify_error` [Listener statistic][2] incremented. +If the `caSecret` field is omitted, Envoy will request but not require client certificates to be present on requests. + +Optionally, you can enable certificate revocation check by providing one or more Certificate Revocation Lists (CRLs). +Attribute `crlSecret` contains a name of an existing Kubernetes Secret that must be of type "Opaque" and have a data key named `crl.pem`. +The data value of the key `crl.pem` must be one or more PEM-encoded CRLs concatenated together. +Large CRL lists are not supported since individual Secrets are limited to 1MiB in size. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: with-client-auth-and-crl-check +spec: + virtualhost: + fqdn: www.example.com + tls: + secretName: secret + clientValidation: + caSecret: client-root-ca + crlSecret: client-crl + routes: + - services: + - name: s1 + port: 80 +``` + +CRLs must be available from all relevant CAs, including intermediate CAs. +Otherwise clients will be denied access, since the revocation status cannot be checked for the full certificate chain. +This behavior can be controlled by `crlOnlyVerifyLeafCert` field. +If the option is set to `true`, only the certificate at the end of the certificate chain will be subject to validation by CRL. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: with-client-auth-and-crl-check-only-leaf +spec: + virtualhost: + fqdn: www.example.com + tls: + secretName: secret + clientValidation: + caSecret: client-root-ca + crlSecret: client-crl + crlOnlyVerifyLeafCert: true + routes: + - services: + - name: s1 + port: 80 +``` + +## Client Certificate Details Forwarding + +HTTPProxy supports passing certificate data through the `x-forwarded-client-cert` header to let applications use details from client certificates (e.g. Subject, SAN...). Since the certificate (or the certificate chain) could exceed the web server header size limit, you have the ability to select what specific part of the certificate to expose in the header through the `forwardClientCertificate` field. Read more about the supported values in the [Envoy documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert). + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: with-client-auth +spec: + virtualhost: + fqdn: www.example.com + tls: + secretName: secret + clientValidation: + caSecret: client-root-ca + forwardClientCertificate: + subject: true + cert: true + chain: true + dns: true + uri: true + routes: + - services: + - name: s1 + port: 80 +``` + +## TLS Session Proxying + +HTTPProxy supports proxying of TLS encapsulated TCP sessions. + +_Note_: The TCP session must be encrypted with TLS. +This is necessary so that Envoy can use SNI to route the incoming request to the correct service. + +If `spec.virtualhost.tls.secretName` is present then that secret will be used to decrypt the TCP traffic at the edge. + +```yaml +# httpproxy-tls-termination.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: example + namespace: default +spec: + virtualhost: + fqdn: tcp.example.com + tls: + secretName: secret + tcpproxy: + services: + - name: tcpservice + port: 8080 + - name: otherservice + port: 9999 + weight: 20 +``` + +The `spec.tcpproxy` key indicates that this _root_ HTTPProxy will forward the de-encrypted TCP traffic to the backend service. + +### TLS Session Passthrough + +If you wish to handle the TLS handshake at the backend service set `spec.virtualhost.tls.passthrough: true` indicates that once SNI demuxing is performed, the encrypted connection will be forwarded to the backend service. +The backend service is expected to have a key which matches the SNI header received at the edge, and be capable of completing the TLS handshake. This is called SSL/TLS Passthrough. + +```yaml +# httpproxy-tls-passthrough.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: example + namespace: default +spec: + virtualhost: + fqdn: tcp.example.com + tls: + passthrough: true + tcpproxy: + services: + - name: tcpservice + port: 8080 + - name: otherservice + port: 9999 + weight: 20 +``` + +[1]: ../configuration#fallback-certificate +[2]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/stats#tls-statistics diff --git a/site/content/docs/1.29/config/tracing.md b/site/content/docs/1.29/config/tracing.md new file mode 100644 index 00000000000..5500c26d547 --- /dev/null +++ b/site/content/docs/1.29/config/tracing.md @@ -0,0 +1,117 @@ +# Tracing Support + +- [Overview](#overview) +- [Tracing-config](#tracing-config) + +## Overview + +Envoy has rich support for [distributed tracing][1],and supports exporting data to third-party providers (Zipkin, Jaeger, Datadog, etc.) + +[OpenTelemetry][2] is a CNCF project which is working to become a standard in the space. It was formed as a merger of the OpenTracing and OpenCensus projects. + +Contour supports configuring envoy to export data to OpenTelemetry, and allows users to customize some configurations. + +- Custom service name, the default is `contour`. +- Custom sampling rate, the default is `100`. +- Custom the maximum length of the request path, the default is `256`. +- Customize span tags from literal or request headers. +- Customize whether to include the pod's hostname and namespace. + +## Tracing-config + +In order to use this feature, you must first select and deploy an opentelemetry-collector to receive the tracing data exported by envoy. + +First we should deploy an opentelemetry-collector to receive the tracing data exported by envoy +```bash +# install operator +kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml +``` + +Install an otel collector instance, with verbose logging exporter enabled: +```shell +kubectl apply -f - </`. If the CA secret's namespace is not the same namespace as the `HTTPProxy` resource, [TLS Certificate Delegation][4] must be used to allow the owner of the CA certificate secret to delegate, for the purposes of referencing the CA certificate in a different namespace, permission to Contour to read the Secret object from another namespace. + +_**Note:** +If `spec.routes.services[].validation` is present, `spec.routes.services[].{name,port}` must point to a Service with a matching `projectcontour.io/upstream-protocol.tls` Service annotation._ + +In the example below, the upstream service is named `secure-backend` and uses port `8443`: + +```yaml +# httpproxy-example.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: example +spec: + virtualhost: + fqdn: www.example.com + routes: + - services: + - name: secure-backend + port: 8443 + validation: + caSecret: my-certificate-authority + subjectName: backend.example.com +``` + +```yaml +# service-secure-backend.yaml +apiVersion: v1 +kind: Service +metadata: + name: secure-backend + annotations: + projectcontour.io/upstream-protocol.tls: "8443" +spec: + ports: + - name: https + port: 8443 + selector: + app: secure-backend + +``` + +If the `validation` spec is defined on a service, but the secret which it references does not exist, Contour will reject the update and set the status of the HTTPProxy object accordingly. +This helps prevent the case of proxying to an upstream where validation is requested, but not yet available. + +```yaml +Status: + Current Status: invalid + Description: route "/": service "tls-nginx": upstreamValidation requested but secret not found or misconfigured +``` + +## Upstream Validation + +When defining upstream services on a route, it's possible to configure the connection from Envoy to the backend endpoint to communicate over TLS. + +A CA certificate and a Subject Name must be provided, which are both used to verify the backend endpoint's identity. + +If specifying multiple Subject Names, `SubjectNames` and `SubjectName` must be configured such that `SubjectNames[0] == SubjectName`. + +The CA certificate bundle for the backend service should be supplied in a Kubernetes Secret. +The referenced Secret must be of type "Opaque" and have a data key named `ca.crt`. +This data value must be a PEM-encoded certificate bundle. + +In addition to the CA certificate and the subject name, the Kubernetes service must also be annotated with a Contour specific annotation: `projectcontour.io/upstream-protocol.tls: ` ([see annotations section][1]). + +_**Note:** This annotation is applied to the Service not the Ingress or HTTPProxy object._ + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: blog + namespace: marketing +spec: + routes: + - services: + - name: s2 + port: 80 + validation: + caSecret: foo-ca-cert + subjectName: foo.marketing + subjectNames: + - foo.marketing + - bar.marketing +``` + +## Envoy Client Certificate + +Contour can be configured with a `namespace/name` in the [Contour configuration file][3] of a Kubernetes secret which Envoy uses as a client certificate when upstream TLS is configured for the backend. +Envoy will send the certificate during TLS handshake when the backend applications request the client to present its certificate. +Backend applications can validate the certificate to ensure that the connection is coming from Envoy. + +[1]: annotations.md +[2]: api/#projectcontour.io/v1.Service +[3]: ../configuration#fallback-certificate +[4]: tls-delegation.md diff --git a/site/content/docs/1.29/config/virtual-hosts.md b/site/content/docs/1.29/config/virtual-hosts.md new file mode 100644 index 00000000000..b7a138dde6b --- /dev/null +++ b/site/content/docs/1.29/config/virtual-hosts.md @@ -0,0 +1,138 @@ +# Virtual Hosts + + +Similar to Ingress, HTTPProxy support name-based virtual hosting. +Name-based virtual hosts use multiple host names with the same IP address. + +``` +foo.bar.com --| |-> foo.bar.com s1:80 + | 178.91.123.132 | +bar.foo.com --| |-> bar.foo.com s2:80 +``` + +Unlike Ingress however, HTTPProxy only support a single root domain per HTTPProxy object. +As an example, this Ingress object: + +```yaml +# ingress-name.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: name-example +spec: + rules: + - host: foo1.bar.com + http: + paths: + - backend: + service: + name: s1 + port: + number: 80 + pathType: Prefix + - host: bar1.bar.com + http: + paths: + - backend: + service: + name: s2 + port: + number: 80 + pathType: Prefix +``` + +must be represented by two different HTTPProxy objects: + +```yaml +# httpproxy-name.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: name-example-foo + namespace: default +spec: + virtualhost: + fqdn: foo1.bar.com + routes: + - services: + - name: s1 + port: 80 +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: name-example-bar + namespace: default +spec: + virtualhost: + fqdn: bar1.bar.com + routes: + - services: + - name: s2 + port: 80 +``` + +A HTTPProxy object that contains a [`virtualhost`][2] field is known as a "root proxy". + +## Virtualhost aliases + +To present the same set of routes under multiple DNS entries (e.g. `www.example.com` and `example.com`), including a service with a `prefix` condition of `/` can be used. + +```yaml +# httpproxy-inclusion-multipleroots.yaml +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: multiple-root + namespace: default +spec: + virtualhost: + fqdn: bar.com + includes: + - name: main + namespace: default +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: multiple-root-www + namespace: default +spec: + virtualhost: + fqdn: www.bar.com + includes: + - name: main + namespace: default +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: main + namespace: default +spec: + routes: + - services: + - name: s2 + port: 80 +``` + +## Restricted root namespaces + +HTTPProxy inclusion allows Administrators to limit which users/namespaces may configure routes for a given domain, but it does not restrict where root HTTPProxies may be created. +Contour has an enforcing mode which accepts a list of namespaces where root HTTPProxy are valid. +Only users permitted to operate in those namespaces can therefore create HTTPProxy with the [`virtualhost`] field ([see API docs][2]). + +This restricted mode is enabled in Contour by specifying a command line flag, `--root-namespaces`, which will restrict Contour to only searching the defined namespaces for root HTTPProxy. This CLI flag accepts a comma separated list of namespaces where HTTPProxy are valid (e.g. `--root-namespaces=default,kube-system,my-admin-namespace`). + +HTTPProxy with a defined [virtualhost][2] field that are not in one of the allowed root namespaces will be flagged as `invalid` and will be ignored by Contour. + +Additionally, when defined, Contour will only watch for Kubernetes secrets in these namespaces ignoring changes in all other namespaces. +Proper RBAC rules should also be created to restrict what namespaces Contour has access matching the namespaces passed to the command line flag. +An example of this is included in the [examples directory][1] and shows how you might create a namespace called `root-httproxy`. + +_**Note:** The restricted root namespace feature is only supported for HTTPProxy CRDs. +`--root-namespaces` does not affect the operation of Ingress objects. In order to limit other resources, see the `--watch-namespaces` configuration flag._ + +[1]: {{< param github_url>}}/tree/{{< param branch >}}/examples/root-rbac +[2]: api/#projectcontour.io/v1.VirtualHost diff --git a/site/content/docs/1.29/config/websockets.md b/site/content/docs/1.29/config/websockets.md new file mode 100644 index 00000000000..136c0468378 --- /dev/null +++ b/site/content/docs/1.29/config/websockets.md @@ -0,0 +1,27 @@ +# Websockets + +WebSocket support can be enabled on specific routes using the `enableWebsockets` field: + +```yaml +# httpproxy-websockets.yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: chat + namespace: default +spec: + virtualhost: + fqdn: chat.example.com + routes: + - services: + - name: chat-app + port: 80 + - conditions: + - prefix: /websocket + enableWebsockets: true # Setting this to true enables websocket for all paths that match /websocket + services: + - name: chat-app + port: 80 +``` + +If you are using Gateway API, websockets are enabled by default at the Listener level. diff --git a/site/content/docs/1.29/configuration.md b/site/content/docs/1.29/configuration.md new file mode 100644 index 00000000000..8e20de93fe5 --- /dev/null +++ b/site/content/docs/1.29/configuration.md @@ -0,0 +1,541 @@ +# Contour Configuration Reference + +- [Serve Flags](#serve-flags) +- [Configuration File](#configuration-file) +- [Environment Variables](#environment-variables) +- [Bootstrap Config File](#bootstrap-config-file) + +## Overview + +There are various ways to configure Contour, flags, the configuration file, as well as environment variables. +Contour has a precedence of configuration for contour serve, meaning anything configured in the config file is overridden by environment vars which are overridden by cli flags. + +## Serve Flags + +The `contour serve` command is the main command which is used to watch for Kubernetes resource and process them into Envoy configuration which is then streamed to any Envoy via its xDS gRPC connection. +There are a number of flags that can be passed to this command which further configures how Contour operates. +Many of these flags are mirrored in the [Contour Configuration File](#configuration-file). + +| Flag Name | Description | +| --------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `--config-path` | Path to base configuration | +| `--contour-config-name` | Name of the ContourConfiguration resource to use | +| `--incluster` | Use in cluster configuration | +| `--kubeconfig=` | Path to kubeconfig (if not in running inside a cluster) | +| `--xds-address=` | xDS gRPC API address | +| `--xds-port=` | xDS gRPC API port | +| `--stats-address=` | Envoy /stats interface address | +| `--stats-port=` | Envoy /stats interface port | +| `--debug-http-address=
` | Address the debug http endpoint will bind to. | +| `--debug-http-port=` | Port the debug http endpoint will bind to | +| `--http-address=` | Address the metrics HTTP endpoint will bind to | +| `--http-port=` | Port the metrics HTTP endpoint will bind to. | +| `--health-address=` | Address the health HTTP endpoint will bind to | +| `--health-port=` | Port the health HTTP endpoint will bind to | +| `--contour-cafile=` | CA bundle file name for serving gRPC with TLS | +| `--contour-cert-file=` | Contour certificate file name for serving gRPC over TLS | +| `--contour-key-file=` | Contour key file name for serving gRPC over TLS | +| `--insecure` | Allow serving without TLS secured gRPC | +| `--root-namespaces=` | Restrict contour to searching these namespaces for root ingress routes | +| `--watch-namespaces=` | Restrict contour to searching these namespaces for all resources | +| `--ingress-class-name=` | Contour IngressClass name (comma-separated list allowed) | +| `--ingress-status-address=
` | Address to set in Ingress object status | +| `--envoy-http-access-log=` | Envoy HTTP access log | +| `--envoy-https-access-log=` | Envoy HTTPS access log | +| `--envoy-service-http-address=` | Kubernetes Service address for HTTP requests | +| `--envoy-service-https-address=` | Kubernetes Service address for HTTPS requests | +| `--envoy-service-http-port=` | Kubernetes Service port for HTTP requests | +| `--envoy-service-https-port=` | Kubernetes Service port for HTTPS requests | +| `--envoy-service-name=` | Name of the Envoy service to inspect for Ingress status details. | +| `--envoy-service-namespace=` | Envoy Service Namespace | +| `--use-proxy-protocol` | Use PROXY protocol for all listeners | +| `--accesslog-format=` | Format for Envoy access logs | +| `--disable-leader-election` | Disable leader election mechanism | +| `--disable-feature=` | Do not start an informer for the specified resources. Flag can be given multiple times. | +| `--leader-election-lease-duration` | The duration of the leadership lease. | +| `--leader-election-renew-deadline` | The duration leader will retry refreshing leadership before giving up. | +| `--leader-election-retry-period` | The interval which Contour will attempt to acquire leadership lease. | +| `--leader-election-resource-name` | The name of the resource (Lease) leader election will lease. | +| `--leader-election-resource-namespace` | The namespace of the resource (Lease) leader election will lease. | +| `-d, --debug` | Enable debug logging | +| `--kubernetes-debug=` | Enable Kubernetes client debug logging | +| `--log-format=` | Log output format for Contour. Either text (default) or json. | +| `--kubernetes-client-qps=` | QPS allowed for the Kubernetes client. | +| `--kubernetes-client-burst=` | Burst allowed for the Kubernetes client. | + +## Configuration File + +A configuration file can be passed to the `--config-path` argument of the `contour serve` command to specify additional configuration to Contour. +In most deployments, this file is passed to Contour via a ConfigMap which is mounted as a volume to the Contour pod. + +The Contour configuration file is optional. +In its absence, Contour will operate with reasonable defaults. +Where Contour settings can also be specified with command-line flags, the command-line value takes precedence over the configuration file. + +| Field Name | Type | Default | Description | +|---------------------------| ---------------------- |------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| accesslog-format | string | `envoy` | This key sets the global [access log format][2] for Envoy. Valid options are `envoy` or `json`. | +| accesslog-format-string | string | None | If present, this specifies custom access log format for Envoy. See [Envoy documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage) for more information about the syntax. This field only has effect if `accesslog-format` is `envoy` | +| accesslog-level | string | `info` | This field specifies the verbosity level of the access log. Valid options are `info` (default, all requests are logged), `error` (all non-success, i.e. 300+ response code, requests are logged), `critical` (all server error, i.e. 500+ response code, requests are logged) and `disabled`. | +| debug | boolean | `false` | Enables debug logging. | +| default-http-versions | string array | HTTP/1.1
HTTP/2 | This array specifies the HTTP versions that Contour should program Envoy to serve. HTTP versions are specified as strings of the form "HTTP/x", where "x" represents the version number. | +| disableAllowChunkedLength | boolean | `false` | If this field is true, Contour will disable the RFC-compliant Envoy behavior to strip the `Content-Length` header if `Transfer-Encoding: chunked` is also set. This is an emergency off-switch to revert back to Envoy's default behavior in case of failures. +| disableMergeSlashes | boolean | `false` | This field disables Envoy's non-standard merge_slashes path transformation behavior that strips duplicate slashes from request URL paths. +| serverHeaderTransformation | string | `overwrite` | This field defines the action to be applied to the Server header on the response path. Values: `overwrite` (default), `append_if_absent`, `pass_through` +| disablePermitInsecure | boolean | `false` | If this field is true, Contour will ignore `PermitInsecure` field in HTTPProxy documents. | +| envoy-service-name | string | `envoy` | This sets the service name that will be inspected for address details to be applied to Ingress objects. | +| envoy-service-namespace | string | `projectcontour` | This sets the namespace of the service that will be inspected for address details to be applied to Ingress objects. If the `CONTOUR_NAMESPACE` environment variable is present, Contour will populate this field with its value. | +| ingress-status-address | string | None | If present, this specifies the address that will be copied into the Ingress status for each Ingress that Contour manages. It is exclusive with `envoy-service-name` and `envoy-service-namespace`. | +| incluster | boolean | `false` | This field specifies that Contour is running in a Kubernetes cluster and should use the in-cluster client access configuration. | +| json-fields | string array | [fields][5] | This is the list the field names to include in the JSON [access log format][2]. This field only has effect if `accesslog-format` is `json`. | +| kubeconfig | string | `$HOME/.kube/config` | Path to a Kubernetes [kubeconfig file][3] for when Contour is executed outside a cluster. | +| kubernetesClientQPS | float32 | | QPS allowed for the Kubernetes client. | +| kubernetesClientBurst | int | | Burst allowed for the Kubernetes client. | +| policy | PolicyConfig | | The default [policy configuration](#policy-configuration). | +| tls | TLS | | The default [TLS configuration](#tls-configuration). | +| timeouts | TimeoutConfig | | The [timeout configuration](#timeout-configuration). | +| cluster | ClusterConfig | | The [cluster configuration](#cluster-configuration). | +| network | NetworkConfig | | The [network configuration](#network-configuration). | +| listener | ListenerConfig | | The [listener configuration](#listener-configuration). | +| server | ServerConfig | | The [server configuration](#server-configuration) for `contour serve` command. | +| gateway | GatewayConfig | | The [gateway-api Gateway configuration](#gateway-configuration). | +| rateLimitService | RateLimitServiceConfig | | The [rate limit service configuration](#rate-limit-service-configuration). | +| enableExternalNameService | boolean | `false` | Enable ExternalName Service processing. Enabling this has security implications. Please see the [advisory](https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc) for more details. | +| metrics | MetricsParameters | | The [metrics configuration](#metrics-configuration) | +| featureFlags | string array | `[]` | Defines the toggle to enable new contour features. Available toggles are:
1. `useEndpointSlices` - configures contour to fetch endpoint data from k8s endpoint slices. | + +### TLS Configuration + +The TLS configuration block can be used to configure default values for how +Contour should provision TLS hosts. + +| Field Name | Type | Default | Description | +| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| minimum-protocol-version | string | `1.2` | This field specifies the minimum TLS protocol version that is allowed. Valid options are `1.2` (default) and `1.3`. Any other value defaults to TLS 1.2. +| maximum-protocol-version | string | `1.3` | This field specifies the maximum TLS protocol version that is allowed. Valid options are `1.2` and `1.3`. Any other value defaults to TLS 1.3. | +| fallback-certificate | | | [Fallback certificate configuration](#fallback-certificate). | +| envoy-client-certificate | | | [Client certificate configuration for Envoy](#envoy-client-certificate). | +| cipher-suites | []string | See [config package documentation](https://pkg.go.dev/github.com/projectcontour/contour/pkg/config#pkg-variables) | This field specifies the TLS ciphers to be supported by TLS listeners when negotiating TLS 1.2. This parameter should only be used by advanced users. Note that this is ignored when TLS 1.3 is in use. The set of ciphers that are allowed is a superset of those supported by default in stock, non-FIPS Envoy builds and FIPS builds as specified [here](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#envoy-v3-api-field-extensions-transport-sockets-tls-v3-tlsparameters-cipher-suites). Custom ciphers not accepted by Envoy in a standard build are not supported. | + +### Upstream TLS Configuration + +The Upstream TLS configuration block can be used to configure default values for how Contour establishes TLS for upstream connections. + +| Field Name | Type | Default | Description | +| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| minimum-protocol-version | string | `1.2` | This field specifies the minimum TLS protocol version that is allowed. Valid options are `1.2` (default) and `1.3`. Any other value defaults to TLS 1.2. | +| maximum-protocol-version | string | `1.3` | This field specifies the maximum TLS protocol version that is allowed. Valid options are `1.2` and `1.3`. Any other value defaults to TLS 1.3. | +| cipher-suites | []string | See [config package documentation](https://pkg.go.dev/github.com/projectcontour/contour/pkg/config#pkg-variables) | This field specifies the TLS ciphers to be supported by TLS listeners when negotiating TLS 1.2. This parameter should only be used by advanced users. Note that this is ignored when TLS 1.3 is in use. The set of ciphers that are allowed is a superset of those supported by default in stock, non-FIPS Envoy builds and FIPS builds as specified [here](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#envoy-v3-api-field-extensions-transport-sockets-tls-v3-tlsparameters-cipher-suites). Custom ciphers not accepted by Envoy in a standard build are not supported. | + +### Fallback Certificate + +| Field Name | Type | Default | Description | +| ---------- | ------ | ------- | ----------------------------------------------------------------------------------------------- | +| name | string | `""` | This field specifies the name of the Kubernetes secret to use as the fallback certificate. | +| namespace | string | `""` | This field specifies the namespace of the Kubernetes secret to use as the fallback certificate. | + + +### Envoy Client Certificate + +| Field Name | Type | Default | Description | +| ---------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | `""` | This field specifies the name of the Kubernetes secret to use as the client certificate and private key when establishing TLS connections to the backend service. | +| namespace | string | `""` | This field specifies the namespace of the Kubernetes secret to use as the client certificate and private key when establishing TLS connections to the backend service. | + + +### Timeout Configuration + +The timeout configuration block can be used to configure various timeouts for the proxies. All fields are optional; Contour/Envoy defaults apply if a field is not specified. + +| Field Name | Type | Default | Description | +| -------------------------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| request-timeout | string | none* | This field specifies the default request timeout. Note that this is a timeout for the entire request, not an idle timeout. Must be a [valid Go duration string][4], or omitted or set to `infinity` to disable the timeout entirely. See [the Envoy documentation][12] for more information.

_Note: A value of `0s` previously disabled this timeout entirely. This is no longer the case. Use `infinity` or omit this field to disable the timeout._ | +| connection-idle-timeout | string | `60s` | This field defines how long the proxy should wait while there are no active requests (for HTTP/1.1) or streams (for HTTP/2) before terminating an HTTP connection. The timeout applies to downstream connections only. Must be a [valid Go duration string][4], or `infinity` to disable the timeout entirely. See [the Envoy documentation][8] for more information. | +| stream-idle-timeout | string | `5m`* | This field defines how long the proxy should wait while there is no activity during single request/response (for HTTP/1.1) or stream (for HTTP/2). Timeout will not trigger while HTTP/1.1 connection is idle between two consecutive requests. Must be a [valid Go duration string][4], or `infinity` to disable the timeout entirely. See [the Envoy documentation][9] for more information. | +| max-connection-duration | string | none* | This field defines the maximum period of time after an HTTP connection has been established from the client to the proxy before it is closed by the proxy, regardless of whether there has been activity or not. Must be a [valid Go duration string][4], or omitted or set to `infinity` for no max duration. See [the Envoy documentation][10] for more information. | +| delayed-close-timeout | string | `1s`* | *Note: this is an advanced setting that should not normally need to be tuned.*

This field defines how long envoy will wait, once connection close processing has been initiated, for the downstream peer to close the connection before Envoy closes the socket associated with the connection. Setting this timeout to 'infinity' will disable it. See [the Envoy documentation][13] for more information. | +| connection-shutdown-grace-period | string | `5s`* | This field defines how long the proxy will wait between sending an initial GOAWAY frame and a second, final GOAWAY frame when terminating an HTTP/2 connection. During this grace period, the proxy will continue to respond to new streams. After the final GOAWAY frame has been sent, the proxy will refuse new streams. Must be a [valid Go duration string][4]. See [the Envoy documentation][11] for more information. | +| connect-timeout | string | `2s` | This field defines how long the proxy will wait for the upstream connection to be established. + +_This is Envoy's default setting value and is not explicitly configured by Contour._ + +### Cluster Configuration + +The cluster configuration block can be used to configure various parameters for Envoy clusters. + +| Field Name | Type | Default | Description | +|-----------------------------------|--------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| dns-lookup-family | string | auto | This field specifies the dns-lookup-family to use for upstream requests to externalName type Kubernetes services from an HTTPProxy route. Values are: `auto`, `v4`, `v6`, `all` | +| max-requests-per-connection | int | none | This field specifies the maximum requests for upstream connections. If not specified, there is no limit | +| circuit-breakers | [CircuitBreakers](#circuit-breakers) | none | This field specifies the default value for [circuit-breaker-annotations](https://projectcontour.io/docs/main/config/annotations/) for services that don't specify them. | +| per-connection-buffer-limit-bytes | int | 1MiB* | This field specifies the soft limit on size of the cluster’s new connection read and write buffer. If not specified, Envoy defaults of 1MiB apply | +| upstream-tls | UpstreamTLS | | [Upstream TLS configuration](#upstream-tls) | + +_This is Envoy's default setting value and is not explicitly configured by Contour._ + + + + +### Network Configuration + +The network configuration block can be used to configure various parameters network connections. + +| Field Name | Type | Default | Description | +| ---------------- | ---- | ------- | ----------------------------------------------------------------------------------------------------------------------- | +| num-trusted-hops | int | 0 | Configures the number of additional ingress proxy hops from the right side of the x-forwarded-for HTTP header to trust. | +| admin-port | int | 9001 | Configures the Envoy Admin read-only listener on Envoy. Set to `0` to disable. | + +### Listener Configuration + +The listener configuration block can be used to configure various parameters for Envoy listener. + +| Field Name | Type | Default | Description | +|-----------------------------------|--------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| connection-balancer | string | `""` | This field specifies the listener connection balancer. If the value is `exact`, the listener will use the exact connection balancer to balance connections between threads in a single Envoy process. See [the Envoy documentation][14] for more information. | +| max-requests-per-connection | int | none | This field specifies the maximum requests for downstream connections. If not specified, there is no limit | +| per-connection-buffer-limit-bytes | int | 1MiB* | This field specifies the soft limit on size of the listener’s new connection read and write buffer. If not specified, Envoy defaults of 1MiB apply | +| socket-options | SocketOptions | | The [Socket Options](#socket-options) for Envoy listeners. | +| max-requests-per-io-cycle | int | none | Defines the limit on number of HTTP requests that Envoy will process from a single connection in a single I/O cycle. Requests over this limit are processed in subsequent I/O cycles. Can be used as a mitigation for CVE-2023-44487 when abusive traffic is detected. Configures the `http.max_requests_per_io_cycle` Envoy runtime setting. The default value when this is not set is no limit. | +| http2-max-concurrent-streams | int | none | Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS Envoy will advertise in the SETTINGS frame in HTTP/2 connections and the limit for concurrent streams allowed for a peer on a single HTTP/2 connection. It is recommended to not set this lower than 100 but this field can be used to bound resource usage by HTTP/2 connections and mitigate attacks like CVE-2023-44487. The default value when this is not set is unlimited. | + +_This is Envoy's default setting value and is not explicitly configured by Contour._ + +### Server Configuration + +The server configuration block can be used to configure various settings for the `contour serve` command. + +| Field Name | Type | Default | Description | +| --------------- | ------ | ------- | ----------------------------------------------------------------------------- | +| xds-server-type | string | envoy | This field specifies the xDS Server to use. Options are `envoy` or `contour` (deprecated). | + +### Gateway Configuration + +The gateway configuration block is used to configure which gateway-api Gateway Contour should configure: + +| Field Name | Type | Default | Description | +| -------------- | -------------- | ------- | ------------------------------------------------------------------------------ | +| gatewayRef | NamespacedName | | [Gateway namespace and name](#gateway-ref). | + +### Gateway Ref + +| Field Name | Type | Default | Description | +| ---------- | ------ | ------- | ----------------------------------------------------------------------------------------------- | +| name | string | `""` | This field specifies the name of the specific Gateway to reconcile. | +| namespace | string | `""` | This field specifies the namespace of the specific Gateway to reconcile. | + +### Policy Configuration + +The Policy configuration block can be used to configure default policy values +that are set if not overridden by the user. + +The `request-headers` field is used to rewrite headers on a HTTP request, and +the `response-headers` field is used to rewrite headers on a HTTP response. + +| Field Name | Type | Default | Description | +| ---------------- | ------------ | ------- | ------------------------------------------------------------------------------------------------- | +| request-headers | HeaderPolicy | none | The default request headers set or removed on all service routes if not overridden in the object | +| response-headers | HeaderPolicy | none | The default response headers set or removed on all service routes if not overridden in the object | +| applyToIngress | Boolean | false | Whether the global policy should apply to Ingress objects | + +#### HeaderPolicy + +The `set` field sets an HTTP header value, creating it if it doesn't already exist but not overwriting it if it does. +The `remove` field removes an HTTP header. + +| Field Name | Type | Default | Description | +| ---------- | ----------------- | ------- | ------------------------------------------------------------------------------- | +| set | map[string]string | none | Map of headers to set on all service routes if not overridden in the object | +| remove | []string | none | List of headers to remove on all service routes if not overridden in the object | + +Note: the values of entries in the `set` and `remove` fields can be overridden in HTTPProxy objects but it is not possible to remove these entries. + +### Rate Limit Service Configuration + +The rate limit service configuration block is used to configure an optional global rate limit service: + +| Field Name | Type | Default | Description | +|-----------------------------| ------ | ------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| extensionService | string | | This field identifies the extension service defining the rate limit service, formatted as /. | +| domain | string | contour | This field defines the rate limit domain value to pass to the rate limit service. Acts as a container for a set of rate limit definitions within the RLS. | +| failOpen | bool | false | This field defines whether to allow requests to proceed when the rate limit service fails to respond with a valid rate limit decision within the timeout defined on the extension service. | +| enableXRateLimitHeaders | bool | false | This field defines whether to include the X-RateLimit headers X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (as defined by the IETF Internet-Draft https://tools.ietf.org/id/draft-polli-ratelimit-headers-03.html), on responses to clients when the Rate Limit Service is consulted for a request. | +| enableResourceExhaustedCode | bool | false | This field defines whether to translate status code 429 to gRPC RESOURCE_EXHAUSTED instead of UNAVAILABLE. | + +### Metrics Configuration + +MetricsParameters holds configurable parameters for Contour and Envoy metrics. + +| Field Name | Type | Default | Description | +| ----------- | ----------------------- | ------- | -------------------------------------------------------------------- | +| contour | MetricsServerParameters | | [Metrics Server Parameters](#metrics-server-parameters) for Contour. | +| envoy | MetricsServerParameters | | [Metrics Server Parameters](#metrics-server-parameters) for Envoy. | + +### Metrics Server Parameters + +MetricsServerParameters holds configurable parameters for Contour and Envoy metrics. +Metrics are served over HTTPS if `server-certificate-path` and `server-key-path` are set. +Metrics and health endpoints cannot have the same port number when metrics are served over HTTPS. + +| Field Name | Type | Default | Description | +| ----------------------- | ------ | ---------------------------- | -----------------------------------------------------------------------------| +| address | string | 0.0.0.0 | Address that metrics server will bind to. | +| port | int | 8000 (Contour), 8002 (Envoy) | Port that metrics server will bind to. | +| server-certificate-path | string | none | Optional path to the server certificate file. | +| server-key-path | string | none | Optional path to the server private key file. | +| ca-certificate-path | string | none | Optional path to the CA certificate file used to verify client certificates. | + +### Socket Options + +| Field Name | Type | Default | Description | +| --------------- | ------ | ------- | ----------------------------------------------------------------------------- | +| tos | int | 0 | Defines the value for IPv4 TOS field (including 6 bit DSCP field) for IP packets originating from Envoy listeners. Single value is applied to all listeners. The value must be in the range 0-255, 0 means socket option is not set. If listeners are bound to IPv6-only addresses, setting this option will cause an error. | +| traffic-class | int | 0 | Defines the value for IPv6 Traffic Class field (including 6 bit DSCP field) for IP packets originating from the Envoy listeners. Single value is applied to all listeners. The value must be in the range 0-255, 0 means socket option is not set. If listeners are bound to IPv4-only addresses, setting this option will cause an error. | + + +### Circuit Breakers + +| Field Name | Type | Default | Description | +| --------------- | ------ | ------- | ----------------------------------------------------------------------------- | +| max-connections | int | 0 | The maximum number of connections that a single Envoy instance allows to the Kubernetes Service; defaults to 1024. | +| max-pending-requests | int | 0 | The maximum number of pending requests that a single Envoy instance allows to the Kubernetes Service; defaults to 1024. | +| max-requests | int | 0 | The maximum parallel requests a single Envoy instance allows to the Kubernetes Service; defaults to 1024 | +| max-retries | int | 0 | The maximum number of parallel retries a single Envoy instance allows to the Kubernetes Service; defaults to 3. This setting only makes sense if the cluster is configured to do retries.| + +### Configuration Example + +The following is an example ConfigMap with configuration file included: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: contour + namespace: projectcontour +data: + contour.yaml: | + # + # server: + # determine which XDS Server implementation to utilize in Contour. + # xds-server-type: envoy + # + # specify the gateway-api Gateway Contour should configure + # gateway: + # namespace: projectcontour + # name: contour + # + # should contour expect to be running inside a k8s cluster + # incluster: true + # + # path to kubeconfig (if not running inside a k8s cluster) + # kubeconfig: /path/to/.kube/config + # + # Disable RFC-compliant behavior to strip "Content-Length" header if + # "Tranfer-Encoding: chunked" is also set. + # disableAllowChunkedLength: false + # Disable HTTPProxy permitInsecure field + disablePermitInsecure: false + tls: + # minimum TLS version that Contour will negotiate + # minimum-protocol-version: "1.2" + # TLS ciphers to be supported by Envoy TLS listeners when negotiating + # TLS 1.2. + # cipher-suites: + # - '[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305]' + # - '[ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]' + # - 'ECDHE-ECDSA-AES256-GCM-SHA384' + # - 'ECDHE-RSA-AES256-GCM-SHA384' + # Defines the Kubernetes name/namespace matching a secret to use + # as the fallback certificate when requests which don't match the + # SNI defined for a vhost. + fallback-certificate: + # name: fallback-secret-name + # namespace: projectcontour + envoy-client-certificate: + # name: envoy-client-cert-secret-name + # namespace: projectcontour + ### Logging options + # Default setting + accesslog-format: envoy + # The default access log format is defined by Envoy but it can be customized by setting following variable. + # accesslog-format-string: "...\n" + # To enable JSON logging in Envoy + # accesslog-format: json + # accesslog-level: info + # The default fields that will be logged are specified below. + # To customise this list, just add or remove entries. + # The canonical list is available at + # https://godoc.org/github.com/projectcontour/contour/internal/envoy#JSONFields + # json-fields: + # - "@timestamp" + # - "authority" + # - "bytes_received" + # - "bytes_sent" + # - "downstream_local_address" + # - "downstream_remote_address" + # - "duration" + # - "method" + # - "path" + # - "protocol" + # - "request_id" + # - "requested_server_name" + # - "response_code" + # - "response_flags" + # - "uber_trace_id" + # - "upstream_cluster" + # - "upstream_host" + # - "upstream_local_address" + # - "upstream_service_time" + # - "user_agent" + # - "x_forwarded_for" + # + # default-http-versions: + # - "HTTP/2" + # - "HTTP/1.1" + # + # The following shows the default proxy timeout settings. + # timeouts: + # request-timeout: infinity + # connection-idle-timeout: 60s + # stream-idle-timeout: 5m + # max-connection-duration: infinity + # connection-shutdown-grace-period: 5s + # + # Envoy cluster settings. + # cluster: + # configure the cluster dns lookup family + # valid options are: auto (default), v4, v6, all + # dns-lookup-family: auto + # the maximum requests for upstream connections. + # If not specified, there is no limit. + # Setting this parameter to 1 will effectively disable keep alive + # max-requests-per-connection: 0 + # the soft limit on size of the cluster’s new connection read and write buffers + # per-connection-buffer-limit-bytes: 32768 + # + # network: + # Configure the number of additional ingress proxy hops from the + # right side of the x-forwarded-for HTTP header to trust. + # num-trusted-hops: 0 + # Configure the port used to access the Envoy Admin interface. + # admin-port: 9001 + # + # Configure an optional global rate limit service. + # rateLimitService: + # Identifies the extension service defining the rate limit service, + # formatted as /. + # extensionService: projectcontour/ratelimit + # Defines the rate limit domain to pass to the rate limit service. + # Acts as a container for a set of rate limit definitions within + # the RLS. + # domain: contour + # Defines whether to allow requests to proceed when the rate limit + # service fails to respond with a valid rate limit decision within + # the timeout defined on the extension service. + # failOpen: false + # Defines whether to include the X-RateLimit headers X-RateLimit-Limit, + # X-RateLimit-Remaining, and X-RateLimit-Reset (as defined by the IETF + # Internet-Draft linked below), on responses to clients when the Rate + # Limit Service is consulted for a request. + # ref. https://tools.ietf.org/id/draft-polli-ratelimit-headers-03.html + # enableXRateLimitHeaders: false + # Defines whether to translate status code 429 to grpc code RESOURCE_EXHAUSTED + # instead of the default UNAVAILABLE + # enableResourceExhaustedCode: false + # + # Global Policy settings. + # policy: + # # Default headers to set on all requests (unless set/removed on the HTTPProxy object itself) + # request-headers: + # set: + # # example: the hostname of the Envoy instance that proxied the request + # X-Envoy-Hostname: %HOSTNAME% + # # example: add a l5d-dst-override header to instruct Linkerd what service the request is destined for + # l5d-dst-override: %CONTOUR_SERVICE_NAME%.%CONTOUR_NAMESPACE%.svc.cluster.local:%CONTOUR_SERVICE_PORT% + # # default headers to set on all responses (unless set/removed on the HTTPProxy object itself) + # response-headers: + # set: + # # example: Envoy flags that provide additional details about the response or connection + # X-Envoy-Response-Flags: %RESPONSE_FLAGS% + # Whether or not the policy settings should apply to ingress objects + # applyToIngress: true + # + # metrics: + # contour: + # address: 0.0.0.0 + # port: 8000 + # server-certificate-path: /path/to/server-cert.pem + # server-key-path: /path/to/server-private-key.pem + # ca-certificate-path: /path/to/root-ca-for-client-validation.pem + # envoy: + # address: 0.0.0.0 + # port: 8002 + # server-certificate-path: /path/to/server-cert.pem + # server-key-path: /path/to/server-private-key.pem + # ca-certificate-path: /path/to/root-ca-for-client-validation.pem + # + # listener: + # connection-balancer: exact + # socket-options: + # tos: 64 + # traffic-class: 64 +``` + +_Note:_ The default example `contour` includes this [file][1] for easy deployment of Contour. + +## Environment Variables + +### CONTOUR_NAMESPACE + +If present, the value of the `CONTOUR_NAMESPACE` environment variable is used as: + +1. The value for the `contour bootstrap --namespace` flag unless otherwise specified. +1. The value for the `contour certgen --namespace` flag unless otherwise specified. +1. The value for the `contour serve --envoy-service-namespace` flag unless otherwise specified. +1. The value for the `contour serve --leader-election-resource-namespace` flag unless otherwise specified. + +The `CONTOUR_NAMESPACE` environment variable is set via the [Downward API][6] in the Contour [example manifests][7]. + +## Bootstrap Config File + +The bootstrap configuration file is generated by an initContainer in the Envoy daemonset which runs the `contour bootstrap` command to generate the file. +This configuration file configures the Envoy container to connect to Contour and receive configuration via xDS. + +The next section outlines all the available flags that can be passed to the `contour bootstrap` command which are used to customize +the configuration file to match the environment in which Envoy is deployed. + +### Bootstrap Flags + +There are flags that can be passed to `contour bootstrap` that help configure how Envoy +connects to Contour: + +| Flag | Default | Description | +| -------------------------------------- |-------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| --resources-dir | "" | Directory where resource files will be written. | +| --admin-address | /admin/admin.sock | Path to Envoy admin unix domain socket. | +| --admin-port (Deprecated) | 9001 | Deprecated: Port is now configured as a Contour flag. | +| --xds-address | 127.0.0.1 | Address to connect to Contour xDS server on. | +| --xds-port | 8001 | Port to connect to Contour xDS server on. | +| --envoy-cafile | "" | CA filename for Envoy secure xDS gRPC communication. | +| --envoy-cert-file | "" | Client certificate filename for Envoy secure xDS gRPC communication. | +| --envoy-key-file | "" | Client key filename for Envoy secure xDS gRPC communication. | +| --namespace | projectcontour | Namespace the Envoy container will run, also configured via ENV variable "CONTOUR_NAMESPACE". Namespace is used as part of the metric names on static resources defined in the bootstrap configuration file. | +| --xds-resource-version | v3 | Currently, the only valid xDS API resource version is `v3`. | +| --dns-lookup-family | auto | Defines what DNS Resolution Policy to use for Envoy -> Contour cluster name lookup. Either v4, v6, auto or all. | +| --log-format | text | Log output format for Contour. Either text or json. | +| --overload-max-heap | 0 | Defines the maximum heap memory of the envoy controlled by the overload manager. When the value is greater than 0, the overload manager is enabled, and when envoy reaches 95% of the maximum heap size, it performs a shrink heap operation. When it reaches 98% of the maximum heap size, Envoy Will stop accepting requests. | + + +[1]: {{< param github_url>}}/tree/{{< param branch >}}/examples/contour/01-contour-config.yaml +[2]: config/access-logging +[3]: https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/ +[4]: https://golang.org/pkg/time/#ParseDuration +[5]: https://godoc.org/github.com/projectcontour/contour/internal/envoy#DefaultFields +[6]: https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/ +[7]: {{< param github_url>}}/tree/{{< param branch >}}/examples/contour +[8]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-idle-timeout +[9]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-stream-idle-timeout +[10]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-field-config-core-v3-httpprotocoloptions-max-connection-duration +[11]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-drain-timeout +[12]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-request-timeout +[13]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-delayed-close-timeout +[14]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener.proto#config-listener-v3-listener-connectionbalanceconfig diff --git a/site/content/docs/1.29/deploy-options.md b/site/content/docs/1.29/deploy-options.md new file mode 100644 index 00000000000..0ae74a53bd9 --- /dev/null +++ b/site/content/docs/1.29/deploy-options.md @@ -0,0 +1,383 @@ +# Deployment Options + +The [Getting Started][8] guide shows you a simple way to get started with Contour on your cluster. +This topic explains the details and shows you additional options. +Most of this covers running Contour using a Kubernetes Service of `Type: LoadBalancer`. +If you don't have a cluster with that capability see the [Running without a Kubernetes LoadBalancer][1] section. + +## Installation + +Contour requires a secret containing TLS certificates that are used to secure the gRPC communication between Contour<>Envoy. +This secret can be auto-generated by the Contour `certgen` job or provided by an administrator. +Traffic must be forwarded to Envoy, typically via a Service of `type: LoadBalancer`. +All other requirements such as RBAC permissions, configuration details, are provided or have good defaults for most installations. + +### Setting resource requests and limits + +It is recommended that resource requests and limits be set on all Contour and Envoy containers. +The example YAML manifests used in the [Getting Started][8] guide do not include these, because the appropriate values can vary widely from user to user. +The table below summarizes the Contour and Envoy containers, and provides some reasonable resource requests to start with (note that these should be adjusted based on observed usage and expected load): + +| Workload | Container | Request (mem) | Request (cpu) | +| ------------------- | ---------------- | ------------- | ------------- | +| deployment/contour | contour | 128Mi | 250m | +| daemonset/envoy | envoy | 256Mi | 500m | +| daemonset/envoy | shutdown-manager | 50Mi | 25m | + + +### Envoy as Daemonset + +The recommended installation is for Contour to run as a Deployment and Envoy to run as a Daemonset. +The example Damonset places a single instance of Envoy per node in the cluster as well as attaches to `hostPorts` on each node. +This model allows for simple scaling of Envoy instances as well as ensuring even distribution of instances across the cluster. + +The [example daemonset manifest][2] or [Contour Gateway Provisioner][12] will create an installation based on these recommendations. + +_Note: If the size of the cluster is scaled down, connections can be lost since Kubernetes Damonsets do not follow proper `preStop` hooks._ + +### Envoy as Deployment + +An alternative Envoy deployment model is utilizing a Kubernetes Deployment with a configured `podAntiAffinity` which attempts to mirror the Daemonset deployment model. +A benefit of this model compared to the Daemonset version is when a node is removed from the cluster, the proper shutdown events are available so connections can be cleanly drained from Envoy before terminating. + +The [example deployment manifest][14] will create an installation based on these recommendations. + +## Testing your installation + +### Get your hostname or IP address + +To retrieve the IP address or DNS name assigned to your Contour deployment, run: + +```bash +$ kubectl get -n projectcontour service envoy -o wide +``` + +On AWS, for example, the response looks like: + +``` +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR +contour 10.106.53.14 a47761ccbb9ce11e7b27f023b7e83d33-2036788482.ap-southeast-2.elb.amazonaws.com 80:30274/TCP 3h app=contour +``` + +Depending on your cloud provider, the `EXTERNAL-IP` value is an IP address, or, in the case of Amazon AWS, the DNS name of the ELB created for Contour. Keep a record of this value. + +Note that if you are running an Elastic Load Balancer (ELB) on AWS, you must add more details to your configuration to get the remote address of your incoming connections. +See the [instructions for enabling the PROXY protocol.][4] + +#### Minikube + +On Minikube, to get the IP address of the Contour service run: + +```bash +$ minikube service -n projectcontour envoy --url +``` + +The response is always an IP address, for example `http://192.168.99.100:30588`. This is used as CONTOUR_IP in the rest of the documentation. + +#### kind + +When creating the cluster on Kind, pass a custom configuration to allow Kind to expose port 80/443 to your local host: + +```yaml +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane +- role: worker + extraPortMappings: + - containerPort: 80 + hostPort: 80 + listenAddress: "0.0.0.0" + - containerPort: 443 + hostPort: 443 + listenAddress: "0.0.0.0" +``` + +Then run the create cluster command passing the config file as a parameter. +This file is in the `examples/kind` directory: + +```bash +$ kind create cluster --config examples/kind/kind-expose-port.yaml +``` + +Then, your CONTOUR_IP (as used below) will just be `localhost:80`. + +_Note: We've created a public DNS record (`local.projectcontour.io`) which is configured to resolve to `127.0.0.1``. This allows you to use a real domain name in your kind cluster._ + +### Test with Ingress + +The Contour repository contains an example deployment of the Kubernetes Up and Running demo application, [kuard][5]. +To test your Contour deployment, deploy `kuard` with the following command: + +```bash +$ kubectl apply -f https://projectcontour.io/examples/kuard.yaml +``` + +Then monitor the progress of the deployment with: + +```bash +$ kubectl get po,svc,ing -l app=kuard +``` + +You should see something like: + +``` +NAME READY STATUS RESTARTS AGE +po/kuard-370091993-ps2gf 1/1 Running 0 4m +po/kuard-370091993-r63cm 1/1 Running 0 4m +po/kuard-370091993-t4dqk 1/1 Running 0 4m + +NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +svc/kuard 10.110.67.121 80/TCP 4m + +NAME HOSTS ADDRESS PORTS AGE +ing/kuard * 10.0.0.47 80 4m +``` + +... showing that there are three Pods, one Service, and one Ingress that is bound to all virtual hosts (`*`). + +In your browser, navigate your browser to the IP or DNS address of the Contour Service to interact with the demo application. + +### Test with HTTPProxy + +To test your Contour deployment with [HTTPProxy][9], run the following command: + +```sh +$ kubectl apply -f https://projectcontour.io/examples/kuard-httpproxy.yaml +``` + +Then monitor the progress of the deployment with: + +```sh +$ kubectl get po,svc,httpproxy -l app=kuard +``` + +You should see something like: + +```sh +NAME READY STATUS RESTARTS AGE +pod/kuard-bcc7bf7df-9hj8d 1/1 Running 0 1h +pod/kuard-bcc7bf7df-bkbr5 1/1 Running 0 1h +pod/kuard-bcc7bf7df-vkbtl 1/1 Running 0 1h + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/kuard ClusterIP 10.102.239.168 80/TCP 1h + +NAME FQDN TLS SECRET FIRST ROUTE STATUS STATUS DESCRIPT +httpproxy.projectcontour.io/kuard kuard.local valid valid HTTPProxy +``` + +... showing that there are three Pods, one Service, and one HTTPProxy . + +In your terminal, use curl with the IP or DNS address of the Contour Service to send a request to the demo application: + +```sh +$ curl -H 'Host: kuard.local' ${CONTOUR_IP} +``` + +## Running without a Kubernetes LoadBalancer + +If you can't or don't want to use a Service of `type: LoadBalancer` there are other ways to run Contour. + +### NodePort Service + +If your cluster doesn't have the capability to configure a Kubernetes LoadBalancer, +or if you want to configure the load balancer outside Kubernetes, +you can change the Envoy Service in the [`02-service-envoy.yaml`][7] file and set `type` to `NodePort`. + +This will have every node in your cluster listen on the resultant port and forward traffic to Contour. +That port can be discovered by taking the second number listed in the `PORT` column when listing the service, for example `30274` in `80:30274/TCP`. + +Now you can point your browser at the specified port on any node in your cluster to communicate with Contour. + +### Host Networking + +You can run Contour without a Kubernetes Service at all. +This is done by having the Envoy pod run with host networking. +Contour's examples utilize this model in the `/examples` directory. +To configure, set: `hostNetwork: true` and `dnsPolicy: ClusterFirstWithHostNet` on your Envoy pod definition. +Next, pass `--envoy-service-http-port=80 --envoy-service-https-port=443` to the contour `serve` command which instructs Envoy to listen directly on port 80/443 on each host that it is running. +This is best paired with a DaemonSet (perhaps paired with Node affinity) to ensure that a single instance of Contour runs on each Node. +See the [AWS NLB tutorial][10] as an example. + +## Disabling Features + +You can run Contour with certain features disabled by passing `--disable-feature` flag to the Contour `serve` command. +The flag is used to disable the informer for a custom resource, effectively making the corresponding CRD optional in the cluster. +You can provide the flag multiple times. + +For example, to disable ExtensionService CRD, use the flag as follows: `--disable-feature=extensionservices`. + +See the [configuration section entry][19] for all options. + +## Upgrading Contour/Envoy + +At times, it's needed to upgrade Contour, the version of Envoy, or both. +The included `shutdown-manager` can assist with watching Envoy for open connections while draining and give signal back to Kubernetes as to when it's fine to delete Envoy pods during this process. + +See the [redeploy envoy][11] docs for more information about how to not drop active connections to Envoy. +Also see the [upgrade guides][15] on steps to roll out a new version of Contour. + +## Running Multiple Instances of Contour + +It's possible to run multiple instances of Contour within a single Kubernetes cluster. +This can be useful for separating external vs. internal ingress, for having separate ingress controllers for different ingress classes, and more. +Each Contour instance can also be configured via the `--watch-namespaces` flag to handle their own namespaces. This allows the Kubernetes RBAC objects +to be restricted further. + +The recommended way to deploy multiple Contour instances is to put each instance in its own namespace. +This avoids most naming conflicts that would otherwise occur, and provides better logical separation between the instances. +However, it is also possible to deploy multiple instances in a single namespace if needed; this approach requires more modifications to the example manifests to function properly. +Each approach is described in detail below, using the [examples/contour][17] directory's manifests for reference. + +### In Separate Namespaces (recommended) + +In general, this approach requires updating the `namespace` of all resources, as well as giving unique names to cluster-scoped resources to avoid conflicts. + +- `00-common.yaml`: + - update the name of the `Namespace` + - update the namespace of both `ServiceAccounts` +- `01-contour-config.yaml`: + - update the namespace of the `ConfigMap` + - if you have any namespaced references within the ConfigMap contents (e.g. `fallback-certificate`, `envoy-client-certificate`), ensure those point to the correct namespace as well. +- `01-crds.yaml` will be shared between the two instances; no changes are needed. +- `02-job-certgen.yaml`: + - update the namespace of all resources + - update the namespace of the `ServiceAccount` subject within the `RoleBinding` +- `02-role-contour.yaml`: + - update the name of the `ClusterRole` to be unique + - update the namespace of the `Role` +- `02-rbac.yaml`: + - update the name of the `ClusterRoleBinding` to be unique + - update the namespace of the `RoleBinding` + - update the namespaces of the `ServiceAccount` subject within both resources + - update the name of the ClusterRole within the ClusterRoleBinding's roleRef to match the unique name used in `02-role-contour.yaml` +- `02-service-contour.yaml`: + - update the namespace of the `Service` +- `02-service-envoy.yaml`: + - update the namespace of the `Service` +- `03-contour.yaml`: + - update the namespace of the `Deployment` + - add an argument to the container, `--ingress-class-name=`, so this instance only processes Ingresses/HTTPProxies with the given ingress class. +- `03-envoy.yaml`: + - update the namespace of the `DaemonSet` + - remove the two `hostPort` definitions from the container (otherwise, these would conflict between the two instances) + + +### In The Same Namespace + +This approach requires giving unique names to all resources to avoid conflicts, and updating all resource references to use the correct names. + +- `00-common.yaml`: + - update the names of both `ServiceAccounts` to be unique +- `01-contour-config.yaml`: + - update the name of the `ConfigMap` to be unique +- `01-crds.yaml` will be shared between the two instances; no changes are needed. +- `02-job-certgen.yaml`: + - update the names of all resources to be unique + - update the name of the `Role` within the `RoleBinding`'s roleRef to match the unique name used for the `Role` + - update the name of the `ServiceAccount` within the `RoleBinding`'s subjects to match the unique name used for the `ServiceAccount` + - update the serviceAccountName of the `Job` + - add an argument to the container, `--secrets-name-suffix=`, so the generated TLS secrets have unique names + - update the spec.template.metadata.labels on the `Job` to be unique +- `02-role-contour.yaml`: + - update the names of the `ClusterRole` and `Role` to be unique +- `02-rbac.yaml`: + - update the names of the `ClusterRoleBinding` and `RoleBinding` to be unique + - update the roleRefs within both resources to reference the unique `Role` and `ClusterRole` names used in `02-role-contour.yaml` + - update the subjects within both resources to reference the unique `ServiceAccount` name used in `00-common.yaml` +- `02-service-contour.yaml`: + - update the name of the `Service` to be unique + - update the selector to be unique (this must match the labels used in `03-contour.yaml`, below) +- `02-service-envoy.yaml`: + - update the name of the `Service` to be unique + - update the selector to be unique (this must match the labels used in `03-envoy.yaml`, below) +- `03-contour.yaml`: + - update the name of the `Deployment` to be unique + - update the metadata.labels, the spec.selector.matchLabels, the spec.template.metadata.labels, and the spec.template.spec.affinity.podAntiAffinity labels to match the labels used in `02-service-contour.yaml` + - update the serviceAccountName to match the unique name used in `00-common.yaml` + - update the `contourcert` volume to reference the unique `Secret` name generated from `02-certgen.yaml` (e.g. `contourcert`) + - update the `contour-config` volume to reference the unique `ConfigMap` name used in `01-contour-config.yaml` + - add an argument to the container, `--leader-election-resource-name=`, so this Contour instance uses a separate leader election `Lease` + - add an argument to the container, `--envoy-service-name=`, referencing the unique name used in `02-service-envoy.yaml` + - add an argument to the container, `--ingress-class-name=`, so this instance only processes Ingresses/HTTPProxies with the given ingress class. +- `03-envoy.yaml`: + - update the name of the `DaemonSet` to be unique + - update the metadata.labels, the spec.selector.matchLabels, and the spec.template.metadata.labels to match the unique labels used in `02-service-envoy.yaml` + - update the `--xds-address` argument to the initContainer to use the unique name of the contour Service from `02-service-contour.yaml` + - update the serviceAccountName to match the unique name used in `00-common.yaml` + - update the `envoycert` volume to reference the unique `Secret` name generated from `02-certgen.yaml` (e.g. `envoycert`) + - remove the two `hostPort` definitions from the container (otherwise, these would conflict between the two instances) + +### Using the Gateway provisioner + +The Contour Gateway provisioner also supports deploying multiple instances of Contour, either in the same namespace or different namespaces. +See [Getting Started with the Gateway provisioner][16] for more information on getting started with the Gateway provisioner. +To deploy multiple Contour instances, you create multiple `Gateways`, either in the same namespace or in different namespaces. + +Note that although the provisioning request itself is made via a Gateway API resource (`Gateway`), this method of installation still allows you to use *any* of the supported APIs for defining virtual hosts and routes: `Ingress`, `HTTPProxy`, or Gateway API's `HTTPRoute` and `TLSRoute`. + +If you are using `Ingress` or `HTTPProxy`, you will likely want to assign each Contour instance a different ingress class, so they each handle different subsets of `Ingress`/`HTTPProxy` resources. +To do this, [create two separate GatewayClasses][18], each with a different `ContourDeployment` parametersRef. +The `ContourDeployment` specs should look like: + +```yaml +kind: ContourDeployment +apiVersion: projectcontour.io/v1alpha1 +metadata: + namespace: projectcontour + name: ingress-class-1 +spec: + runtimeSettings: + ingress: + classNames: + - ingress-class-1 +--- +kind: ContourDeployment +apiVersion: projectcontour.io/v1alpha1 +metadata: + namespace: projectcontour + name: ingress-class-2 +spec: + runtimeSettings: + ingress: + classNames: + - ingress-class-2 +``` + +Then create each `Gateway` with the appropriate `spec.gatewayClassName`. + +## Running Contour in tandem with another ingress controller + +If you're running multiple ingress controllers, or running on a cloudprovider that natively handles ingress, +you can specify the annotation `kubernetes.io/ingress.class: "contour"` on all ingresses that you would like Contour to claim. +You can customize the class name with the `--ingress-class-name` flag at runtime. (A comma-separated list of class names is allowed.) +If the `kubernetes.io/ingress.class` annotation is present with a value other than `"contour"`, Contour will ignore that ingress. + +## Uninstall Contour + +To remove Contour or the Contour Gateway Provisioner from your cluster, delete the namespace: + +```bash +$ kubectl delete ns projectcontour +``` +**Note**: Your namespace may differ from above. + +[1]: #running-without-a-kubernetes-loadbalancer +[2]: {{< param github_url>}}/tree/{{< param branch >}}/examples/render/contour.yaml +[3]: #host-networking +[4]: guides/proxy-proto.md +[5]: https://github.com/kubernetes-up-and-running/kuard +[7]: {{< param github_url>}}/tree/{{< param branch >}}/examples/contour/02-service-envoy.yaml +[8]: /getting-started +[9]: config/fundamentals.md +[10]: guides/deploy-aws-nlb.md +[11]: redeploy-envoy.md +[12]: {{< param github_url>}}/tree/{{< param branch >}}/examples/render/contour-gateway-provisioner.yaml +[13]: https://projectcontour.io/resources/deprecation-policy/ +[14]: {{< param github_url>}}/tree/{{< param branch >}}/examples/render/contour-deployment.yaml +[15]: /resources/upgrading/ +[16]: https://projectcontour.io/getting-started/#option-3-contour-gateway-provisioner-alpha +[17]: {{< param github_url>}}/tree/{{< param branch >}}/examples/contour +[18]: guides/gateway-api/#next-steps +[19]: configuration.md \ No newline at end of file diff --git a/site/content/docs/1.29/github.md b/site/content/docs/1.29/github.md new file mode 100644 index 00000000000..8a0f36b4f4d --- /dev/null +++ b/site/content/docs/1.29/github.md @@ -0,0 +1,80 @@ +This document outlines how we use GitHub. + +## Milestones + +Contour attempts to ship on a quarterly basis. +These releases are tracked with a milestone. +The _current_ release is the milestone with the closest delivery date. + +Issues which are not assigned to the current milestone _should not be worked on_. + +## Priorities + +This project has three levels of priority: + +- p0 - Must fix immediately. +This is reserved for bugs and security issues. A milestone cannot ship with open p0 issues. +- p1 - Should be done. +p1 issues assigned to a milestone _should_ be completed during that milestone. +- p2 - May be done. +p2 issues assigned to a milestone _may_ be completed during that milestone if time permits. + +Issues without a priority are _unprioritised_. Priority will be assigned by a PM or release manager during issue triage. + +## Questions + +We encourage support questions via issues. +Questions will be tagged `question` and are not assigned a milestone or a priority. + +## Waiting for information + +Any issue which lacks sufficient information for triage will be tagged `waiting-for-info`. +Issues with this tag may be closed after a reasonable length of time if further information is not forthcoming. + +## Issue tagging + +Issues without tags have not be triaged. + +During issue triage, usually by a project member, release manager, or pm, one or more tags will be assigned. + +- `Needs-Product` indicates the issue needs attention by a product owner or PM. +- `Needs-design-doc` indicates the issue requires a design document to be circulated. + +These are blocking states, these labels must be resolved, either by PM or agreeing on a design. + +## Assigning an issue + +Issues within a milestone _should_ be assigned to an owner when work commences on them. +Assigning an issue indicates that you are working on it. + +Before you start to work on an issue you should assign yourself. +From that point onward you are responsible for the issue and you are expected to report timely status on the issue to anyone that asks. + +If you cease work on an issue, even if incomplete, you should leave a comment to that effect on the issue and remove yourself as the assignee. +From that point onward you are no longer responsible for the issue, however you may be approached as a subject matter expert--as the last person to touch the issue--by future assignees. + +For infrequent contributors who are not members of the Contour project, assign yourself by leaving a comment to that effect on the issue. + +*Do not hoard issues, you won't enjoy it* + +## Requesting a review + +PRs which are related to issues in the current milestone should be assigned to the current milestone. +This is an indicator to reviewers that the PR is ready for review and should be reviewed in the current milestone. +Occasionally PRs may be assigned to the next milestone indicating they are for review at the start of the next development cycle. + +All PRs should reference the issue they relate to either by one of the following; + +- `Fixes #NNNN` indicating that merging this issue will fix issue #NNNN +- `Updates #NNNN` indicating that merging this issue will progress issue #NNNN to some degree. + +If there is no `Updates` or `Fixes` line in the PR the review will, with the exception of trivial or self evident fixes, be deferred. + +[Further reading][1] + +## Help wanted and good first issues + +The `help wanted` and `good first issue` tags _may_ be assigned to issues _in the current milestone_. +To limit the amount of work in progress, `help wanted` and `good first issue` should not be used for issues outside the current milestone. + +[1]: https://dave.cheney.net/2019/02/18/talk-then-code \ No newline at end of file diff --git a/site/content/docs/1.29/grpc-tls-howto.md b/site/content/docs/1.29/grpc-tls-howto.md new file mode 100644 index 00000000000..51770de950d --- /dev/null +++ b/site/content/docs/1.29/grpc-tls-howto.md @@ -0,0 +1,169 @@ +# Enabling TLS between Envoy and Contour + +This document describes the steps required to secure communication between Envoy and Contour. +The outcome of this is that we will have two Secrets available in the `projectcontour` namespace: + +- **contourcert:** contains Contour's keypair which is used for serving TLS secured gRPC, and the CA's public certificate bundle which is used for validating Envoy's client certificate. +Contour's certificate must be a valid certificate for the name `contour` in order for this to work. +This is currently hardcoded by Contour. +- **envoycert:** contains Envoy's keypair which used as a client for connecting to Contour, and the CA's public certificate bundle which is used for validating Contour's server certificate. + +Note that both Secrets contain a copy of the CA certificate bundle under the `ca.crt` data key. + +## Ways you can get the certificates into your cluster + +- Deploy the Job from [certgen.yaml][1]. +This will run `contour certgen --kube --secrets-format=compact` for you. +- Run `contour certgen --kube` locally. +- Run the manual procedure below. + +## Caveats and warnings + +**Be very careful with your production certificates!** + +This is intended as an example to help you get started. +For any real deployment, you should **carefully** manage all the certificates and control who has access to them. +Make sure you don't commit them to any git repositories either. + +## Manual TLS certificate generation process + +### Generating a CA keypair + +First, we need to generate a keypair: + +``` +$ openssl req -x509 -new -nodes \ + -keyout certs/cakey.pem -sha256 \ + -days 1825 -out certs/cacert.pem \ + -subj "/O=Project Contour/CN=Contour CA" +``` + +Then, the new CA key will be stored in `certs/cakey.pem` and the cert in `certs/cacert.pem`. + +### Generating Contour's keypair + +Next, we need to generate a keypair for Contour. +First, we make a new private key: + +``` +$ openssl genrsa -out certs/contourkey.pem 2048 +``` + +Then, we create a CSR and have our CA sign the CSR and issue a certificate. +This uses the file [certs/cert-contour.ext][2], which ensures that at least one of the valid names of the certificate is the bareword `contour`. +This is required for the handshake to succeed, as `contour bootstrap` configures Envoy to pass this as the SNI server name for the connection. + +``` +$ openssl req -new -key certs/contourkey.pem \ + -out certs/contour.csr \ + -subj "/O=Project Contour/CN=contour" + +$ openssl x509 -req -in certs/contour.csr \ + -CA certs/cacert.pem \ + -CAkey certs/cakey.pem \ + -CAcreateserial \ + -out certs/contourcert.pem \ + -days 1825 -sha256 \ + -extfile certs/cert-contour.ext +``` + +At this point, the contour certificate and key are in the files `certs/contourcert.pem` and `certs/contourkey.pem` respectively. + +### Generating Envoy's keypair + +Next, we generate a keypair for Envoy: + +``` +$ openssl genrsa -out certs/envoykey.pem 2048 +``` + +Then, we generate a CSR and have the CA sign it: + +``` +$ openssl req -new -key certs/envoykey.pem \ + -out certs/envoy.csr \ + -subj "/O=Project Contour/CN=envoy" + +$ openssl x509 -req -in certs/envoy.csr \ + -CA certs/cacert.pem \ + -CAkey certs/cakey.pem \ + -CAcreateserial \ + -out certs/envoycert.pem \ + -days 1825 -sha256 \ + -extfile certs/cert-envoy.ext +``` + +Like the Contour certificate, this CSR uses the file [certs/cert-envoy.ext][3]. +However, in this case, there are no special names required. + +### Putting the certificates in the cluster + +Next, we create the required Secrets in the target Kubernetes cluster: + +```bash +$ kubectl create secret -n projectcontour generic contourcert \ + --from-file=tls.key=./certs/contourkey.pem \ + --from-file=tls.crt=./certs/contourcert.pem \ + --from-file=ca.crt=./certs/cacert.pem \ + --save-config + +$ kubectl create secret -n projectcontour generic envoycert \ + --from-file=tls.key=./certs/envoykey.pem \ + --from-file=tls.crt=./certs/envoycert.pem \ + --from-file=ca.crt=./certs/cacert.pem \ + --save-config +``` + +Note that we don't put the CA **key** into the cluster, there's no reason for that to be there, and that would create a security problem. + +## Rotating Certificates + +Eventually the certificates that Contour and Envoy use will need to be rotated. +The following steps can be taken to replace the certificates that Contour and Envoy are using: + +1. Generate a new keypair for both Contour and Envoy (optionally also for the CA) +2. Update the Secrets that hold the gRPC TLS keypairs +3. Contour and Envoy will automatically rotate their certificates after mounted secrets have been updated by the kubelet + +The secrets can be updated in-place by running: + +```bash +$ kubectl create secret -n projectcontour generic contourcert \ + --from-file=tls.key=./certs/contourkey.pem \ + --from-file=tls.crt=./certs/contourcert.pem \ + --from-file=ca.crt=./certs/cacert.pem \ + --dry-run -o json \ + | kubectl apply -f - + +$ kubectl create secret -n projectcontour generic envoycert \ + --from-file=tls.key=./certs/envoykey.pem \ + --from-file=tls.crt=./certs/envoycert.pem \ + --from-file=ca.crt=./certs/cacert.pem \ + --dry-run -o json \ + | kubectl apply -f - +``` + +There are few preconditions that need to be met before Envoy can automatically reload certificate and key files: + +- Envoy must be version v1.14.1 or later +- The bootstrap configuration must be generated with `contour bootstrap` using the `--resources-dir` argument, see [examples/contour/03-envoy.yaml][4] + +### Rotate using the contour-certgen job + +When using the built-in Contour certificate generation, the following steps can be used: + +1. Delete the contour-certgen job + - `kubectl delete job contour-certgen -n projectcontour` +2. Reapply the contour-certgen job from [certgen.yaml][1] + +## Conclusion + +Once this process is done, the certificates will be present as Secrets in the `projectcontour` namespace, as required by +[examples/contour][5]. + +[1]: {{< param github_url >}}/tree/{{< param branch >}}/examples/contour/02-job-certgen.yaml +[2]: {{< param github_url >}}/tree/{{< param branch >}}/certs/cert-contour.ext +[3]: {{< param github_url >}}/tree/{{< param branch >}}/certs/cert-envoy.ext +[4]: {{< param github_url >}}/tree/{{< param branch >}}/examples/contour/03-envoy.yaml +[5]: {{< param github_url >}}/tree/{{< param branch >}}/examples/contour + diff --git a/site/content/docs/1.29/guides/_index.md b/site/content/docs/1.29/guides/_index.md new file mode 100644 index 00000000000..8981b8fbd79 --- /dev/null +++ b/site/content/docs/1.29/guides/_index.md @@ -0,0 +1,9 @@ +--- +title: Guides +description: Contour Resources +id: guides +--- +## Getting things done with Contour + +This page contains links to articles on configuring specific Contour features. + diff --git a/site/content/docs/1.29/guides/cert-manager.md b/site/content/docs/1.29/guides/cert-manager.md new file mode 100644 index 00000000000..0f926946eda --- /dev/null +++ b/site/content/docs/1.29/guides/cert-manager.md @@ -0,0 +1,670 @@ +--- +title: Deploying HTTPS services with Contour and cert-manager +--- + +This tutorial shows you how to securely deploy an HTTPS web application on a Kubernetes cluster, using: + +- Kubernetes +- Contour, as the Ingress controller +- [JetStack's cert-manager][1] to provision TLS certificates from [the Let's Encrypt project][6] + +## Prerequisites + +- A Kubernetes cluster deployed in either a data center or a cloud provider with a Kubernetes as a service offering. This tutorial was last tested on a GKE cluster running Kubernetes 1.22 +- RBAC enabled on your cluster +- Your cluster must be able to request a public IP address from your cloud provider, using a load balancer. If you're on AWS or GKE this is automatic if you deploy a Kubernetes service object of type: LoadBalancer. If you're on your own datacenter you must set it up yourself +- A DNS domain that you control, where you host your web application +- Administrator permissions for all deployment steps + +**NOTE:** To use a local cluster like `minikube` or `kind`, see the instructions in [the deployment guide][7]. + +## Summary + +This tutorial walks you through deploying: + +1. [Contour][0] +2. [Jetstack cert-manager][1] +3. A sample web application using HTTPProxy + +**NOTE:** If you encounter failures related to permissions, make sure the user you are operating as has administrator permissions. + +After you've been through the steps the first time, you don't need to repeat deploying Contour and cert-manager for subsequent application deployments. Instead, you can skip to step 3. + +## 1. Deploy Contour + +Run: + +```bash +$ kubectl apply -f {{< param base_url >}}/quickstart/contour.yaml +``` + +to set up Contour as a deployment in its own namespace, `projectcontour`, and tell the cloud provider to provision an external IP that is forwarded to the Contour pods. + +Check the progress of the deployment with this command: + +```bash +$ kubectl -n projectcontour get po +NAME READY STATUS RESTARTS AGE +contour-5475898957-jh9fm 1/1 Running 0 39s +contour-5475898957-qlbs2 1/1 Running 0 39s +contour-certgen-v1.19.0-5xthf 0/1 Completed 0 39s +envoy-hqbkm 2/2 Running 0 39s +``` + +After all the `contour` & `envoy` pods reach `Running` status and fully `Ready`, move on to the next step. + +### Access your cluster + +Retrieve the external address of the load balancer assigned to Contour's Envoys by your cloud provider: + +```bash +$ kubectl get -n projectcontour service envoy -o wide +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR +envoy LoadBalancer 10.51.245.99 35.189.26.87 80:30111/TCP,443:30933/TCP 38d app=envoy +``` + +The value of `EXTERNAL-IP` varies by cloud provider. In this example GKE gives a bare IP address; AWS gives you a long DNS name. + +To make it easier to work with the external load balancer, the tutorial adds a DNS record to a domain we control that points to this load balancer's IP address: + +```bash +$ host gke.davecheney.com +gke.davecheney.com has address 35.189.26.87 +``` + +On AWS, you specify a `CNAME`, not an `A` record, and it would look something like this: + +```bash +$ host aws.davecheney.com +aws.davecheney.com is an alias for a4d1766f6ce1611e7b27f023b7e83d33–1465548734.ap-southeast-2.elb.amazonaws.com. +a4d1766f6ce1611e7b27f023b7e83d33–1465548734.ap-southeast-2.elb.amazonaws.com has address 52.63.20.117 +a4d1766f6ce1611e7b27f023b7e83d33–1465548734.ap-southeast-2.elb.amazonaws.com has address 52.64.233.204 +``` + +In your own data center, you need to arrange for traffic from a public IP address to be forwarded to the cluster IP of the Contour service. This is beyond the scope of the tutorial. + +### Testing connectivity + +You must deploy at least one Ingress object before Contour can configure Envoy to serve traffic. +Note that as a security feature, Contour does not configure Envoy to expose a port to the internet unless there's a reason it should. +For this tutorial we deploy a version of Kenneth Reitz's [httpbin.org service][3]. + +To deploy httpbin to your cluster, run this command: + +```bash +$ kubectl apply -f {{< param base_url >}}/examples/httpbin.yaml +``` + +Check that the pods are running: + +```bash +$ kubectl get po -l app=httpbin +NAME READY STATUS RESTARTS AGE +httpbin-85777b684b-8sqw5 1/1 Running 0 24s +httpbin-85777b684b-pb26w 1/1 Running 0 24s +httpbin-85777b684b-vpgwl 1/1 Running 0 24s +``` + +Then type the DNS name you set up in the previous step into a web browser, for example `http://gke.davecheney.com/`. You should see something like: + +![httpbin screenshot][8] + +You can delete the httpbin service now, or at any time, by running: + +```bash +$ kubectl delete -f {{< param base_url >}}/examples/httpbin.yaml +``` + +## 2. Deploy jetstack/cert-manager + +**NOTE:** cert-manager is a powerful product that provides more functionality than this tutorial demonstrates. +There are plenty of [other ways to deploy cert-manager][4], but they are out of scope. + +### Fetch the source manager deployment manifest + +To keep things simple, we skip cert-manager's Helm installation, and use the [supplied YAML manifests][5]. + +```bash +$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.5.4/cert-manager.yaml +``` + +When cert-manager is up and running you should see something like: + +```bash +$ kubectl -n cert-manager get all +NAME READY STATUS RESTARTS AGE +pod/cert-manager-cainjector-74bb68d67c-8lb2f 1/1 Running 0 40s +pod/cert-manager-f7f8bf74d-65ld9 1/1 Running 0 40s +pod/cert-manager-webhook-645b8bdb7-2h5t6 1/1 Running 0 40s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/cert-manager ClusterIP 10.48.13.252 9402/TCP 40s +service/cert-manager-webhook ClusterIP 10.48.7.220 443/TCP 40s + +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/cert-manager 1/1 1 1 40s +deployment.apps/cert-manager-cainjector 1/1 1 1 40s +deployment.apps/cert-manager-webhook 1/1 1 1 40s + +NAME DESIRED CURRENT READY AGE +replicaset.apps/cert-manager-cainjector-74bb68d67c 1 1 1 40s +replicaset.apps/cert-manager-f7f8bf74d 1 1 1 40s +replicaset.apps/cert-manager-webhook-645b8bdb7 1 1 1 40s +``` + +### Deploy the Let's Encrypt cluster issuer + +cert-manager supports two different CRDs for configuration, an `Issuer`, which is scoped to a single namespace, +and a `ClusterIssuer`, which is cluster-wide. + +For Contour to be able to serve HTTPS traffic for an Ingress in any namespace, use `ClusterIssuer`. +Create a file called `letsencrypt-staging.yaml` with the following contents: + +```yaml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging + namespace: cert-manager +spec: + acme: + email: user@example.com + privateKeySecretRef: + name: letsencrypt-staging + server: https://acme-staging-v02.api.letsencrypt.org/directory + solvers: + - http01: + ingress: + class: contour +``` + +replacing `user@example.com` with your email address. +This is the email address that Let's Encrypt uses to communicate with you about certificates you request. + +The staging Let's Encrypt server is not bound by [the API rate limits of the production server][2]. +This approach lets you set up and test your environment without worrying about rate limits. +You can then repeat this step for a production Let's Encrypt certificate issuer. + +After you edit and save the file, deploy it: + +```bash +$ kubectl apply -f letsencrypt-staging.yaml +clusterissuer.cert-manager.io/letsencrypt-staging created +``` + +Wait for the `ClusterIssuer` to be ready: + +```bash +$ kubectl get clusterissuer letsencrypt-staging +NAME READY AGE +letsencrypt-staging True 54s +``` + +## 3. Deploy your first HTTPS site using Ingress + +For this tutorial we deploy a version of Kenneth Reitz's [httpbin.org service][3]. +We start with the deployment. +Copy the following to a file called `deployment.yaml`: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: httpbin + name: httpbin +spec: + replicas: 1 + selector: + matchLabels: + app: httpbin + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: httpbin + spec: + containers: + - image: docker.io/kennethreitz/httpbin + name: httpbin + ports: + - containerPort: 8080 + name: http + command: ["gunicorn"] + args: ["-b", "0.0.0.0:8080", "httpbin:app"] + dnsPolicy: ClusterFirst +``` + +Deploy to your cluster: + +```bash +$ kubectl apply -f deployment.yaml +deployment.apps/httpbin created +$ kubectl get pod -l app=httpbin +NAME READY STATUS RESTARTS AGE +httpbin-67fd96d97c-8j2rr 1/1 Running 0 56m +``` + +Expose the deployment to the world with a Service. Create a file called `service.yaml` with +the following contents: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: httpbin +spec: + ports: + - port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app: httpbin +``` + +and deploy: + +```bash +$ kubectl apply -f service.yaml +service/httpbin created +$ kubectl get service httpbin +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +httpbin ClusterIP 10.48.6.155 8080/TCP 57m +``` + +Expose the Service to the world with Contour and an Ingress object. Create a file called `ingress.yaml` with +the following contents: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: httpbin +spec: + rules: + - host: httpbin.davecheney.com + http: + paths: + - pathType: Prefix + path: / + backend: + service: + name: httpbin + port: + number: 8080 +``` + +The host name, `httpbin.davecheney.com` is a `CNAME` to the `gke.davecheney.com` record that was created in the first section, and must be created in the same place as the `gke.davecheney.com` record was. +That is, in your cloud provider. +This lets requests to `httpbin.davecheney.com` resolve to the external IP address of the Contour service. +They are then forwarded to the Contour pods running in the cluster: + +```bash +$ host httpbin.davecheney.com +httpbin.davecheney.com is an alias for gke.davecheney.com. +gke.davecheney.com has address 35.189.26.87 +``` + +Change the value of `spec.rules.host` to something that you control, and deploy the Ingress to your cluster: + +```bash +$ kubectl apply -f ingress.yaml +ingress.networking.k8s.io/httpbin created +$ kubectl get ingress httpbin +NAME CLASS HOSTS ADDRESS PORTS AGE +httpbin httpbin.davecheney.com 80 12s +``` + +Now you can type the host name of the service into a browser, or use curl, to verify it's deployed and everything is working: + +```bash +$ curl http://httpbin.davecheney.com/get +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Content-Length": "0", + "Host": "htpbin.davecheney.com", + "User-Agent": "curl/7.58.0", + "X-Envoy-Expected-Rq-Timeout-Ms": "15000", + "X-Envoy-Internal": "true" + }, + "origin": "10.152.0.2", + "url": "http://httpbin.davecheney.com/get" +} +``` + +Excellent, it looks like everything is up and running serving traffic over HTTP. + +### Request a TLS certificate from Let's Encrypt + +Now it's time to use cert-manager to request a TLS certificate from Let's Encrypt. +Do this by adding some annotations and a `tls:` section to the Ingress spec. + +We need to add the following annotations: + +- `cert-manager.io/cluster-issuer: letsencrypt-staging`: tells cert-manager to use the `letsencrypt-staging` cluster issuer you just created. +- `kubernetes.io/tls-acme: "true"`: Tells cert-manager to do ACME TLS (what Let's Encrypt uses). +- `ingress.kubernetes.io/force-ssl-redirect: "true"`: tells Contour to redirect HTTP requests to the HTTPS site. +- `kubernetes.io/ingress.class: contour`: Tells Contour that it should handle this Ingress object. + +Using `kubectl edit ingress httpbin`: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: httpbin + annotations: + cert-manager.io/cluster-issuer: letsencrypt-staging + ingress.kubernetes.io/force-ssl-redirect: "true" + kubernetes.io/ingress.class: contour + kubernetes.io/tls-acme: "true" +spec: + tls: + - secretName: httpbin + hosts: + - httpbin.davecheney.com + rules: + - host: httpbin.davecheney.com + http: + paths: + - pathType: Prefix + path: / + backend: + service: + name: httpbin + port: + number: 8080 +``` + +The certificate is issued in the name of the hosts listed in the `tls:` section, `httpbin.davecheney.com` and stored in the secret `httpbin`. +Behind the scenes, cert-manager creates a certificate CRD to manage the lifecycle of the certificate, and then a series of other CRDs to handle the challenge process. + +You can watch the progress of the certificate as it's issued: + +```bash +$ kubectl describe certificate httpbin | tail -n 12 +Status: + Conditions: + Last Transition Time: 2019-11-07T00:37:55Z + Message: Waiting for CertificateRequest "httpbinproxy-1925286939" to complete + Reason: InProgress + Status: False + Type: Ready +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal GeneratedKey 26s cert-manager Generated a new private key + Normal Requested 26s cert-manager Created new CertificateRequest resource "httpbinproxy-1925286939" +``` + +Wait for the certificate to be issued: + +```bash +$ kubectl describe certificate httpbin | grep -C3 "Certificate is up to date" +Status: + Conditions: + Last Transition Time: 2019-11-06T23:47:50Z + Message: Certificate is up to date and has not expired + Reason: Ready + Status: True + Type: Ready +``` + +A `kubernetes.io/tls` secret is created with the `secretName` specified in the `tls:` field of the Ingress. + +```bash +$ kubectl get secret httpbin +NAME TYPE DATA AGE +httpbin kubernetes.io/tls 2 3m +``` + +cert-manager manages the contents of the secret as long as the Ingress is present in your cluster. + +You can now visit your site, replacing `http://` with `https://` — and you get a huge security warning! +This is because the certificate was issued by the Let's Encrypt staging servers and has a fake CA. +This is so you can't accidentally use the staging servers to serve real certificates. + +```bash +$ curl https://httpbin.davecheney.com/get +curl: (60) SSL certificate problem: unable to get local issuer certificate +More details here: https://curl.haxx.se/docs/sslcerts.html + +curl failed to verify the legitimacy of the server and therefore could not +establish a secure connection to it. To learn more about this situation and +how to fix it, please visit the web page mentioned above. +``` + +### Switch to Let's Encrypt Production + +To request a properly signed certificate from the Let's Encrypt production servers, we create a new `ClusterIssuer`, as before but with some modifications. + +Create a file called `letsencrypt-prod.yaml` with the following contents: + +```yaml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod + namespace: cert-manager +spec: + acme: + email: user@example.com + privateKeySecretRef: + name: letsencrypt-prod + server: https://acme-v02.api.letsencrypt.org/directory + solvers: + - http01: + ingress: + class: contour +``` + +again replacing `user@example.com` with your email address. + +Deploy: + +```bash +$ kubectl apply -f letsencrypt-prod.yaml +clusterissuer.cert-manager.io/letsencrypt-prod created +``` + +Now we use `kubectl edit ingress httpbin` to edit our Ingress to ask for a real certificate from `letsencrypt-prod`: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: httpbin + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ... +``` + +The certificate resource will transition to `Ready: False` while it's re-provisioned from the Let's Encrypt production servers, and then back to `Ready: True` once it's been provisioned: + +```bash +$ kubectl describe certificate httpbin +... +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + ... + Normal Issuing 21s cert-manager Issuing certificate as Secret was previously issued by ClusterIssuer.cert-manager.io/letsencrypt-staging + Normal Reused 21s cert-manager Reusing private key stored in existing Secret resource "httpbin" + Normal Requested 21s cert-manager Created new CertificateRequest resource "httpbin-sjqbt" + Normal Issuing 18s (x2 over 48s) cert-manager The certificate has been successfully issued +``` + +Followed by: + +```bash +$ kubectl get certificate httpbin -o wide +NAME READY SECRET ISSUER STATUS AGE +httpbin True httpbin letsencrypt-prod Certificate is up to date and has not expired 3m35s +``` + +Now revisiting our `https://httpbin.davecheney.com` site should show a valid, trusted, HTTPS certificate. + +```bash +$ curl https://httpbin.davecheney.com/get +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Content-Length": "0", + "Host": "httpbin.davecheney.com", + "User-Agent": "curl/7.58.0", + "X-Envoy-Expected-Rq-Timeout-Ms": "15000", + "X-Envoy-Internal": "true" + }, + "origin": "10.152.0.2", + "url": "https://httpbin.davecheney.com/get" +} +``` + +![httpbin.davecheney.com screenshot][9] + +## Making cert-manager work with HTTPProxy + +cert-manager currently does not have a way to interact directly with HTTPProxy objects in order to respond to the HTTP01 challenge (See [#950][10] and [#951][11] for details). +cert-manager, however, can be configured to request certificates automatically using a `Certificate` object. + +When cert-manager finds a `Certificate` object, it will implement the HTTP01 challenge by creating a new, temporary Ingress object that will direct requests from Let's Encrypt to temporary pods called 'solver pods'. +These pods know how to respond to Let's Encrypt's challenge process for verifying you control the domain you're issuing certificates for. +The Ingress resource as well as the solver pods are short lived and will only be available during the certificate request or renewal process. + +The result of the work steps described previously is a TLS secret, which can be referenced by a HTTPProxy. + +## Details + +To do this, we first need to create our HTTPProxy and Certificate objects. + +This example uses the hostname `httpbinproxy.davecheney.com`, remember to create that name before starting. + +Firstly, the HTTPProxy: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: httpbinproxy +spec: + virtualhost: + fqdn: httpbinproxy.davecheney.com + tls: + secretName: httpbinproxy + routes: + - services: + - name: httpbin + port: 8080 +``` + +This object will be marked as Invalid by Contour, since the TLS secret doesn't exist yet. +Once that's done, create the Certificate object: + +```yaml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: httpbinproxy +spec: + commonName: httpbinproxy.davecheney.com + dnsNames: + - httpbinproxy.davecheney.com + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + secretName: httpbinproxy +``` + +Wait for the Certificate to be provisioned: + +```bash +$ kubectl get certificate httpbinproxy -o wide +NAME READY SECRET ISSUER STATUS AGE +httpbinproxy True httpbinproxy letsencrypt-prod Certificate is up to date and has not expired 39s +``` + +Once cert-manager has fulfilled the HTTP01 challenge, you will have a `httpbinproxy` secret, that will contain the keypair. +Contour will detect that the Secret exists and generate the HTTPProxy config. + +After that, you should be able to curl the new site: + +```bash +$ curl https://httpbinproxy.davecheney.com/get +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Content-Length": "0", + "Host": "httpbinproxy.davecheney.com", + "User-Agent": "curl/7.54.0", + "X-Envoy-Expected-Rq-Timeout-Ms": "15000", + "X-Envoy-External-Address": "122.106.57.183" + }, + "origin": "122.106.57.183", + "url": "https://httpbinproxy.davecheney.com/get" +} +``` + +## Wrapping up + +Now that you've deployed your first HTTPS site using Contour and Let's Encrypt, deploying additional TLS enabled services is much simpler. +Remember that for each HTTPS website you deploy, cert-manager will create a Certificate CRD that provides the domain name and the name of the target Secret. +The TLS functionality will be enabled when the HTTPProxy contains the `tls:` stanza, and the referenced secret contains a valid keypair. + +See the [cert-manager docs][12] for more information. + +## Bonus points + +For bonus points, you can use a feature of Contour to automatically upgrade any HTTP request to the corresponding HTTPS site so you are no longer serving any traffic over insecure HTTP. + +To enable the automatic redirect from HTTP to HTTPS, add this annotation to your Ingress object. + +``` +metadata: + annotations: + ingress.kubernetes.io/force-ssl-redirect: "true" +``` +Now any requests to the insecure HTTP version of your site get an unconditional 301 redirect to the HTTPS version: + +``` +$ curl -v http://httpbin.davecheney.com/get +* Trying 35.189.26.87… +* TCP_NODELAY set +* Connected to httpbin.davecheney.com (35.189.26.87) port 80 (#0) +> GET /get HTTP/1.1 +> Host: httpbin.davecheney.com +> User-Agent: curl/7.58.0 +> Accept: */* +> +< HTTP/1.1 301 Moved Permanently +< location: https://httpbin.davecheney.com/get +< date: Tue, 20 Feb 2018 04:11:46 GMT +< server: envoy +< content-length: 0 +< +* Connection #0 to host httpbin.davecheney.com left intact +``` + +__Note:__ For HTTPProxy resources this happens automatically without the need for an annotation. + +[0]: {{< param github_url >}} +[1]: https://github.com/jetstack/cert-manager +[2]: https://letsencrypt.org/docs/rate-limits/ +[3]: http://httpbin.org/ +[4]: https://docs.cert-manager.io/en/latest/getting-started/install/kubernetes.html +[5]: https://github.com/jetstack/cert-manager/releases/download/v1.5.4/cert-manager.yaml +[6]: https://letsencrypt.org/getting-started/ +[7]: ../deploy-options/#get-your-hostname-or-ip-address +[8]: /img/cert-manager/httpbinhomepage.png +[9]: /img/cert-manager/httpbin.png +[10]: {{< param github_url >}}/issues/950 +[11]: {{< param github_url >}}/issues/951 +[12]: https://cert-manager.io/docs/usage/ingress/ diff --git a/site/content/docs/1.29/guides/deploy-aws-nlb.md b/site/content/docs/1.29/guides/deploy-aws-nlb.md new file mode 100644 index 00000000000..af3f8df1019 --- /dev/null +++ b/site/content/docs/1.29/guides/deploy-aws-nlb.md @@ -0,0 +1,47 @@ +--- +title: Deploying Contour on AWS with NLB +--- + +This is an advanced deployment guide to configure Contour on AWS with the [Network Load Balancer (NLB)][1]. +This configuration has several advantages: + +1. NLBs are often cheaper. This is especially true for development. Idle LBs do not cost money. +2. There are no extra network hops. Traffic goes to the NLB, to the node hosting Contour, and then to the target pod. +3. Source IP addresses are retained. Envoy (running as part of Contour) sees the native source IP address and records this with an `X-Forwarded-For` header. + +## Moving parts + +- We run Envoy as a DaemonSet across the cluster and Contour as a deployment +- The Envoy pod runs on host ports 80 and 443 on the node +- Host networking means that traffic hits Envoy without transitioning through any other fancy networking hops +- Contour also binds to 8001 for Envoy->Contour config traffic. + +## Deploying Contour + +1. [Clone the Contour repository][4] and cd into the repo +2. Edit the Envoy service (`02-service-envoy.yaml`) in the `examples/contour` directory: + - Remove the existing annotation: `service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp` + - Add the following annotation: `service.beta.kubernetes.io/aws-load-balancer-type: nlb` +3. Run `kubectl apply -f examples/contour` + +This creates the `projectcontour` Namespace along with a ServiceAccount, RBAC rules, Contour Deployment and an Envoy DaemonSet. +It also creates the NLB based loadbalancer for you. + +You can get the address of your NLB via: + +``` +$ kubectl get service envoy --namespace=projectcontour -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' +``` + +## Test + +You can now test your NLB. + +1. Install a workload (see the kuard example in the [main deployment guide][2]). +2. Look up the address for your NLB in the AWS console and enter it in your browser. + - Notice that Envoy fills out `X-Forwarded-For`, because it was the first to see the traffic directly from the browser. + +[1]: https://aws.amazon.com/blogs/aws/new-network-load-balancer-effortless-scaling-to-millions-of-requests-per-second/ +[2]: ../deploy-options/#testing-your-installation +[3]: https://github.com/kubernetes/kubernetes/issues/52173 +[4]: {{< param github_url >}}/tree/{{< param branch >}} diff --git a/site/content/docs/1.29/guides/deploy-aws-tls-nlb.md b/site/content/docs/1.29/guides/deploy-aws-tls-nlb.md new file mode 100644 index 00000000000..7f4f83f685e --- /dev/null +++ b/site/content/docs/1.29/guides/deploy-aws-tls-nlb.md @@ -0,0 +1,135 @@ +--- +title: AWS Network Load Balancer TLS Termination with Contour +--- + +## Motivation + +![diagram illustrating connection between network load balancer and contour](/img/aws-nlb-tls/fig.jpg){:class="img-fluid"} + +Managing TLS certificates (and related configuration) for production cluster workloads is both time consuming, and high risk. For example, storing multiple copies of a certificate secret key in the cluster may increases the chances of it being compromised. Additionally, TLS can be complicated to configure and implement properly. + +Traditionally, TLS termination at the load balancer step required using more expensive application load balancers (ALBs). AWS introduced TLS termination for network load balancers (NLBs) for enhanced security and cost effectiveness. + +The TLS implementation used by the AWS NLB is formally verified and maintained. Additionally, AWS Certificate Manager (ACM) is used, fully isolating your cluster from access to the private key. + +## Solution Overview + +An external client transmits a request to the NLB. The request is encrypted with TLS using the production (e.g., client facing) certificate, and on port 443. + +The NLB decrypts the request, and transmits it on to Envoy running in your cluster on port 8080. It follows the standard request routing configured within the cluster. Notably, the request received within the cluster includes the actual origin IP address of the external client. + +Alternate ports may be configured. End-to-end encryption technically requires the segment between the NLB and cluster pods be encrypted also. A follow-up post will describe the NLB originating TLS based on a cluster certificate. + +## Steps + +### Prerequisites + +1. Access to DNS records for domain name. + +[Review the docs on registering domains with AWS's Route 53.](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/registrar.html) + +An alternate DNS provider may be used, such as Google Domains or Namecheap. + +Later, a subdomain (e.g., demo-service.gcline.us) will be created, pointing to the NLB. Additionally, access to the DNS records is required to generate a TLS certificate for use by the NLB. + +3. Verify [Contour is installed in the cluster.](https://projectcontour.io/getting-started/) + +4. Install [AWS Load Balancer Controller.]( https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/installation/) + +Generally, setting up the Load Balancer Controller has two steps: enabling IAM roles for service accounts, and adding the controller to the cluster. The IAM role allows the controller in the Kubernetes cluster to manage AWS resources. [Learn more about IAM roles for service accounts.](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) + +### Configure + +1. Generate TLS Certificate + +Create a public TLS certificate for the domain using AWS Certificate Manager (ACM). This is streamlined when the domain is managed by Route 53. Review the [AWS Certificate Manager Docs.](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html#request-public-console) + +The domain name on the TLS certificate must correspond to the planned domain name for the kubernetes service. The domain name may be specified explicitly (e.g., tls-demo.gcline.us), or a wildcard certificate can be used (e.g., *.gcline.us). + +If the domain is registered with Route53, the TLS certificate request will automatically be approved. Otherwise, follow ACM console the instructions to create a DNS record to validate the domain. + +After validation, the certificate will be available for use in your AWS account. + +Note the ARN of the certificate, which uniquely identifies it in kubernetes config files. + +![screenshot indicating location of ARN value in web console](/img/aws-nlb-tls/acm-arn.png){:class="img-fluid"} + +2. Create Envoy Service with new NLB + +Contour expects a kubernetes service pointing to Envoy. Add annotations to the service to enable NLB TLS termination, before the traffic reaches Envoy. The annotations are actioned by the load balancer controller. [Review all the NLB annotations on GitHub.](https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/service/annotations/) + +| annotation name | value | meaning | +| ----- | --- | ----- | +| service.beta.kubernetes.io/aws-load-balancer-type | external | explicitly requires an NLB, instead of an ALB | +| service.beta.kubernetes.io/aws-load-balancer-nlb-target-type | ip | route traffic directly to the pod IP | +| service.beta.kubernetes.io/aws-load-balancer-scheme | internet-facing | An internet-facing load balancer has a publicly resolvable DNS name | +| service.beta.kubernetes.io/aws-load-balancer-ssl-cert | "arn:aws:acm:..." | identifies the TLS certificate used by the NLB | +| service.beta.kubernetes.io/aws-load-balancer-ssl-ports | 443 | determines the port the NLB should listen for TLS traffic on| + +Example: + +``` +apiVersion: v1 +kind: Service +metadata: + name: envoy + namespace: projectcontour + annotations: + service.beta.kubernetes.io/aws-load-balancer-type: external + service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip + service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing + service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "arn:aws:acm:us-east-2:185309785115:certificate/7610ed7d-5a81-4ea2-a18a-7ba1606cca3e" + service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443" +spec: + externalTrafficPolicy: Local + ports: + - port: 443 + targetPort: 8080 + name: http + protocol: TCP + selector: + app: envoy + type: LoadBalancer +``` + +*Note:* Don't modify an existing service to add NLB TLS termination. This may result in unexpected behavior, such as duplicate NLB resources or incorrect NLB configuration. + +3. Configure DNS + +**Get domain name using kubectl.** + +The service name and namespace were defined above. + +``` +kubectl get svc envoy --namespace projectcontour +``` + +``` +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +envoy LoadBalancer 10.100.24.154 a7ea2bbde8a164036a7e4c1ed5700cdf-154fb911d990bb1f.elb.us-east-2.amazonaws.com 443:31606/TCP 40d +``` + +Note the last 4 digits of the domain name for the NLB. For example, "bb1f". + +**Setup DNS alias for NLB** + +Create a DNS record pointing from a friendly name (e.g., tls-demo.gcline.us) to the NLB domain (e.g., bb1f.elb.us-east-2.amazonaws.com). + +For AWS's Route 53, follow the instructions below. If you use a different DNS provider, follow their instructions for [creating a CNAME record](https://docs.digitalocean.com/products/networking/dns/how-to/manage-records/#cname-records). + +First, create a new record in Route 53. + +Use the "A" record type, and enable the ["alias" option.](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values-alias.html) This option attaches the DNS record to the AWS resource, without requiring an extra lookup step for clients. + +Select the NLB resource. Double check the region, and use the last 4 digits (noted earlier) to select the proper resource. + +![screenshot of Route 53 New Record Console](/img/aws-nlb-tls/record.png){:class="img-fluid"} + +### Verify + +Attempt to access the NLB domain at port 443 with HTTPS/TLS. Is the connection successful? What certificate is used? Does it reach the expected endpoint within the cluster? + +### Next Steps + +Create a second TLS certificate within the cluster, for securing connections between the NLB and pods. A guide on this topic is forthcoming. + diff --git a/site/content/docs/1.29/guides/external-authorization.md b/site/content/docs/1.29/guides/external-authorization.md new file mode 100644 index 00000000000..74076ede518 --- /dev/null +++ b/site/content/docs/1.29/guides/external-authorization.md @@ -0,0 +1,538 @@ +--- +title: External Authorization Support +--- + +Starting in version 1.9, Contour supports routing client requests to an +external authorization server. This feature can be used to centralize +client authorization so that applications don't have to implement their +own authorization mechanisms. + +## Authorization Architecture + +An external authorization server is a server that implements the Envoy +external authorization [GRPC protocol][3]. Contour supports any server +that implements this protocol. + +You can bind an authorization server to Contour by creating a +[`ExtensionService`][4] resource. +This resource tells Contour the service exists, and that it should +program Envoy with an upstream cluster directing traffic to it. +Note that the `ExtensionService` resource just binds the server; at this +point Contour doesn't assume that the server is an authorization server. + +Once you have created `ExtensionService` resource, you can bind it to a +particular application by referencing it in a [`HTTPProxy`][5] resource. +In the `virtualhost` field, a new `authorization` field specifies the name +of an `ExtensionService` to bind for the virtual host. +When you specify a resource name here, Contour will program Envoy to +send authorization checks to the extension service cluster before routing +the request to the upstream application. + +## Authorization Request Flow + +It is helpful to have a mental model of how requests flow through the various +servers involved in authorizing HTTP requests. +The flow diagram below shows the actors that participate in the successful +authorization of an HTTP request. +Note that in some cases, these actors can be combined into a single +application server. +For example, there is no requirement for the external authorization server to +be a separate application from the authorization provider. + + +

+client authorization sequence diagram +

+ +A HTTP Client generates an HTTP request and sends it to +an Envoy instance that Contour has programmed with an external +authorization configuration. +Envoy holds the HTTP request and sends an authorization check request +to the Authorization server that Contour has bound to the virtual host. +The Authorization server may be able to verify the request locally, but in +many cases it will need to make additional requests to an Authorization +Provider server to verify or obtain an authorization token. + +In this flow, the ExtAuth server is able to authorize the request, and sends an +authorization response back to the Proxy. +The response includes the authorization status, and a set of HTTP headers +modifications to make to the HTTP request. +Since this authorization was successful, the Proxy modifies the request and +forwards it to the application. +If the authorization was not successful, the Proxy would have immediately +responded to the client with an HTTP error. + +## Using the Contour Authorization Server + +The Contour project has built a simple authorization server named +[`contour-authserver`][1]. `contour-authserver` supports an authorization +testing server, and an HTTP basic authorization server that accesses +credentials stored in [htpasswd][2] format. + +To get started, ensure that Contour is deployed and that you have +[cert-manager][6] installed in your cluster so that you can easily issue +self-signed TLS certificates. + +At this point, we should also create a cluster-wide self-signed certificate +issuer, just to make it easier to provision TLS certificates later: + +```bash +$ kubectl apply -f - <` with the appropriate version of Go and BoringCrypto, see [here][10] for version specifics): + +```bash +make container BUILD_CGO_ENABLED=1 BUILD_BASE_IMAGE=goboring/golang: BUILD_EXTRA_GO_LDFLAGS="-linkmode=external -extldflags=-static" +``` + +The command above can be broken down as follows: +- `make container` invokes the container image build target +- `BUILD_CGO_ENABLED=1` ensures `cgo` is enabled in the Contour compilation process +- `BUILD_BASE_IMAGE=goboring/golang:` ensures we use the BoringCrypto flavor of Go +- `BUILD_EXTRA_GO_LDFLAGS` contains the additional linker flags we need to perform a static build + - `-linkmode=external` tells the Go linker to use an external linker + - `-extldflags=-static"` passes the `-static` flag to the external link to ensure a statically linked executable is produced + +The container image build process should fail before export of the `contour` binary to the final image if the compiled binary is not statically linked. + +### Validation + +To be fully sure the produced `contour` binary has been compiled with BoringCrypto you must remove the `-s` flag from the base Contour `Makefile` to stop stripping symbols and run through the build process above. +Then you will be able to inspect the `contour` binary with `go tool nm` to check for symbols containing the string `_Cfunc__goboringcrypto_`. +Also, you can use the program [rsc.io/goversion][21]. It will report the crypto implementation used by a given binary when invoked with the `-crypto` flag. + +Once you have a `projectcontour/contour` image built, you can re-tag it if needed, push the image to a registry, and reference it in a Contour deployment to use it! + +## Building Envoy + +Envoy has support for building in a FIPS compliant mode as [documented here][11]. +The upstream project does not distribute a FIPS compliant Envoy container image, but combining the documented process with the processes for building the Envoy executable and container image, we can produce one. + +Again we will need the Envoy source code checked out to the version to build and Docker installed on your computer. +The simplest way to build Envoy without having to learn [Bazel][12] and set up a C++ toolchain on your computer is to build using the Envoy build container image which contains the necessary tools pre-installed. +Note that if you do build with FIPS mode outside of the build container, you can only do so on a Linux-amd64 architecture. + +We can first compile the Envoy binary by running the following in a `bash` shell from the Envoy source directory: + +```bash +BAZEL_BUILD_EXTRA_OPTIONS="--define boringssl=fips" ENVOY_DOCKER_BUILD_DIR= ./ci/run_envoy_docker.sh './ci/do_ci.sh bazel.release //test/exe:envoy_static_test' +``` + +*This command mimics the Envoy release CI process with the target `bazel.release` but differs in only running a single test for brevity. You may omit the `//test/exe:envoy_static_test` test entirely to run the full suite of Envoy tests.* + +Replace `` with a directory you would like the build output to be placed on your host computer. + +Once that build completes, you should have a file named `release.tar.zst` in your specified output directory. +This file is a [Zstandard](https://github.com/facebook/zstd) compressed archive containing the compiled Envoy release and debug binaries. +If you would like to build an image with Envoy according to your own specifications, you can unpack the resulting archive and you will find a stripped Envoy binary in the root and an unstripped Envoy binary with debug info in the `dbg` directory. + +To build an image matching the canonical Envoy upstream release image ([`envoyproxy/envoy`][13]), run the following: + +*Note: You will need a recent version of Docker/BuildKit that supports Zstandard decompression.* + +```bash +# Make ./linux/amd64 directories. +mkdir -p ./linux/amd64 +# Copy Zstandard archive from build step. +cp -a /envoy/x64/bin/release.tar.zst ./linux/amd64/release.tar.zst +# Run the Docker image build. +docker build -f ./ci/Dockerfile-envoy --target envoy . +``` + +Once you have an image built, you can tag it as needed, push the image to a registry, and use it in an Envoy deployment. + +## Configuring TLS Ciphers + +Now that we have Contour and Envoy compiled with BoringCrypto, we can turn our attention to ensuring encrypted communication paths in Contour are configured to use FIPS approved cryptographic algorithms. +Using a FIPS flavor of Envoy will do most of the heavy lifting here without any user configuration needed. + +The critical communication paths and how they are set up to be FIPS compliant are enumerated below: +- Contour -> k8s API + - Contour uses [`client-go`][14] to communicate with the k8s API + - `client-go` uses the default Golang cipher suites configuration + - When compiled with BoringCrypto Go, this set of ciphers is FIPS compliant and not configurable by users +- Envoy -> Contour xDS Server, extension services, upstream services + - A FIPS compliant build of Envoy will choose FIPS approved TLS ciphers when negotiating TLS 1.2 as documented [here][15] + - The set of ciphers is not configurable +- TLS client -> Envoy + - As of [Contour 1.13.0][16], the ciphers Envoy will accept as a server when negotiating TLS 1.2 are configurable + - The [default set of ciphers Contour configures][17] includes some ciphers that are not FIPS approved + - Users must configure FIPS approved ciphers from the list [here][15] + +[0]: https://csrc.nist.gov/publications/detail/fips/140/2/final +[1]: https://csrc.nist.gov/projects/testing-laboratories +[2]: https://boringssl.googlesource.com/boringssl/ +[3]: https://boringssl.googlesource.com/boringssl/+/master/crypto/fipsmodule/FIPS.md +[4]: https://go.googlesource.com/go/+/dev.boringcrypto/README.boringcrypto.md +[5]: https://hub.docker.com/r/projectcontour/contour +[6]: https://www.gnu.org/software/make/ +[7]: https://www.docker.com/ +[8]: {{< param github_url >}}/blob/main/Dockerfile +[9]: https://hub.docker.com/r/goboring/golang/ +[10]: https://go.googlesource.com/go/+/dev.boringcrypto/misc/boring/README.md#version-strings +[11]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ssl.html#fips-140-2 +[12]: https://bazel.build/ +[13]: https://hub.docker.com/r/envoyproxy/envoy +[14]: https://github.com/kubernetes/client-go +[15]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#envoy-v3-api-field-extensions-transport-sockets-tls-v3-tlsparameters-cipher-suites +[16]: https://github.com/projectcontour/contour/releases/tag/v1.13.0 +[17]: https://pkg.go.dev/github.com/projectcontour/contour/pkg/config#pkg-variables +[18]: https://pkg.go.dev/internal/goexperiment@go1.19 +[19]: https://go-boringcrypto.storage.googleapis.com/ +[20]: https://go.googlesource.com/go/+/dev.boringcrypto/misc/boring/README.md#releases +[21]: https://godoc.org/rsc.io/goversion \ No newline at end of file diff --git a/site/content/docs/1.29/guides/gatekeeper.md b/site/content/docs/1.29/guides/gatekeeper.md new file mode 100644 index 00000000000..fa6d01cab41 --- /dev/null +++ b/site/content/docs/1.29/guides/gatekeeper.md @@ -0,0 +1,456 @@ +--- +title: Using Gatekeeper as a validating admission controller with Contour +--- + +This tutorial demonstrates how to use [Gatekeeper](https://github.com/open-policy-agent/gatekeeper) as a validating admission controller for Contour. + +Gatekeeper is a project that enables users to define flexible policies for Kubernetes resources using [Open Policy Agent (OPA)](https://www.openpolicyagent.org/) that are enforced when those resources are created/updated via the Kubernetes API. + +The benefits of using Gatekeeper with Contour are: +- Immediate feedback for the user when they try to create an `HTTPProxy` with an invalid spec. Instead of having to check the `HTTPProxy`'s status after creation for a possible error message, the create is rejected and the user is immediately provided with a reason for the rejection. +- User-defined policies for `HTTPProxy` specs. For example, the Contour admin can define policies to enforce maximum limits on timeouts and retries, disallow certain FQDNs, etc. + +## Prerequisites + +- A Kubernetes cluster with a minimum version of 1.14 (to enable webhook timeouts for Gatekeeper). +- Cluster-admin permissions + +## Deploy Contour + +Run: + +```bash +$ kubectl apply -f {{< param base_url >}}/quickstart/contour.yaml +``` + +This creates a `projectcontour` namespace and sets up Contour as a deployment and Envoy as a daemonset, with communication between them secured by mutual TLS. + +Check the status of the Contour pods with this command: + +```bash +$ kubectl -n projectcontour get pods -l app=contour +NAME READY STATUS RESTARTS AGE +contour-8596d6dbd7-9nrg2 1/1 Running 0 32m +contour-8596d6dbd7-mmtc8 1/1 Running 0 32m +``` + +If installation was successful, all pods should reach `Running` status shortly. + +## Deploy Gatekeeper + +The following instructions are summarized from the [Gatekeeper documentation](https://github.com/open-policy-agent/gatekeeper#installation-instructions). +If you already have Gatekeeper running in your cluster, you can skip this section. + +Run: + +```bash +$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml +``` + +This creates a `gatekeeper-system` namespace and sets up the Gatekeeper controller manager and audit deployments using the latest Gatekeeper release. + +Check the status of the Gatekeeper pods with this command: + +```bash +$ kubectl -n gatekeeper-system get pods +NAME READY STATUS RESTARTS AGE +gatekeeper-audit-67dfc46db6-kjcmc 1/1 Running 0 40m +gatekeeper-controller-manager-7cbc758844-64hhn 1/1 Running 0 40m +gatekeeper-controller-manager-7cbc758844-c4dkd 1/1 Running 0 40m +gatekeeper-controller-manager-7cbc758844-xv9jn 1/1 Running 0 40m +``` + +If installation was successful, all pods should reach `Running` status shortly. + +## Configure Gatekeeper + +### Background + +Gatekeeper uses the [OPA Constraint Framework](https://github.com/open-policy-agent/frameworks/tree/master/constraint) to define and enforce policies. +This framework has two key types: `ConstraintTemplate` and `Constraint`. +A `ConstraintTemplate` defines a reusable OPA policy, along with the parameters that can be passed to it when it is instantiated. +When a `ConstraintTemplate` is created, Gatekeeper automatically creates a custom resource definition (CRD) to represent it in the cluster. + +A `Constraint` is an instantiation of a `ConstraintTemplate`, which tells Gatekeeper to apply it to specific Kubernetes resource types (e.g. `HTTPProxy`) and provides any relevant parameter values. +A `Constraint` is defined as an instance of the CRD representing the associated `ConstraintTemplate`. + +We'll now look at some examples to make these concepts concrete. + +### Configure resource caching + +First, Gatekeeper needs to be configured to store all `HTTPProxy` resources in its internal cache, so that existing `HTTPProxy` resources can be referenced within constraint template policies. +This is essential for being able to define constraints that look across all `HTTPProxies` -- for example, to verify FQDN uniqueness. + +Create a file called `config.yml` containing the following YAML: + +```yaml +apiVersion: config.gatekeeper.sh/v1alpha1 +kind: Config +metadata: + name: config + namespace: "gatekeeper-system" +spec: + sync: + syncOnly: + - group: "projectcontour.io" + version: "v1" + kind: "HTTPProxy" +``` + +Apply it to the cluster: + +```bash +$ kubectl apply -f config.yml +``` + +Note that if you already had Gatekeeper running in your cluster, you may already have the `Config` resource defined. +In that case, you'll need to edit the existing resource to add `HTTPProxy` to the `spec.sync.syncOnly` list. + +### Configure HTTPProxy validations + +The first constraint template and constraint that we'll define are what we'll refer to as a **validation**. +These are rules for `HTTPProxy` specs that Contour universally requires to be true. +In this example, we'll define a constraint template and constraint to enforce that all `HTTPProxies` must have a unique FQDN. + +Create a file called `unique-fqdn-template.yml` containing the following YAML: + +```yaml +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: httpproxyuniquefqdn +spec: + crd: + spec: + names: + kind: HTTPProxyUniqueFQDN + listKind: HTTPProxyUniqueFQDNList + plural: HTTPProxyUniqueFQDNs + singular: HTTPProxyUniqueFQDN + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package httpproxy.uniquefqdn + + violation[{"msg": msg, "other": sprintf("%v/%v", [other.metadata.namespace, other.metadata.name])}] { + got := input.review.object.spec.virtualhost.fqdn + other := data.inventory.namespace[_]["projectcontour.io/v1"]["HTTPProxy"][_] + other.spec.virtualhost.fqdn = got + + not same(other, input.review.object) + msg := "HTTPProxy must have a unique spec.virtualhost.fqdn" + } + + same(a, b) { + a.metadata.namespace == b.metadata.namespace + a.metadata.name == b.metadata.name + } +``` + +Apply it to the cluster: + +```bash +$ kubectl apply -f unique-fqdn-template.yml +``` + +Within a few seconds, you'll see that a corresponding CRD has been created in the cluster: + +```bash +$ kubectl get crd httpproxyuniquefqdn.constraints.gatekeeper.sh +NAME CREATED AT +httpproxyuniquefqdn.constraints.gatekeeper.sh 2020-08-13T16:08:57Z +``` + +Now, create a file called `unique-fqdn-constraint.yml` containing the following YAML: + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: HTTPProxyUniqueFQDN +metadata: + name: httpproxy-unique-fqdn +spec: + match: + kinds: + - apiGroups: ["projectcontour.io"] + kinds: ["HTTPProxy"] +``` + +Note that the `Kind` of this resource corresponds to the new CRD. + +Apply it to the cluster: + +```bash +$ kubectl apply -f unique-fqdn-constraint.yml +``` + +Now, let's create some `HTTPProxies` to see the validation in action. + +Create a file called `httpproxies.yml` containing the following YAML: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: demo + namespace: default +spec: + virtualhost: + fqdn: demo.projectcontour.io +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: demo2 + namespace: default +spec: + virtualhost: + fqdn: demo.projectcontour.io +``` + +Note that both `HTTPProxies` have the same FQDN. + +Apply the YAML: + +```bash +$ kubectl apply -f httpproxies.yml +``` + +You should see something like: +``` +httpproxy.projectcontour.io/demo created +Error from server ([denied by httpproxy-unique-fqdn] HTTPProxy must have a unique FQDN): error when creating "httpproxies.yml": admission webhook "validation.gatekeeper.sh" denied the request: [denied by httpproxy-unique-fqdn] HTTPProxy must have a unique FQDN +``` + +The first `HTTPProxy` was created successfully, because there was not already an existing proxy with the `demo.projectcontour.io` FQDN. +However, when the second `HTTPProxy` was submitted, Gatekeeper rejected its creation because it used the same FQDN as the first one. + +### Configure HTTPProxy policies + +The next constraint template and constraint that we'll create are what we refer to as a **policy**. +These are rules for `HTTPProxy` specs that an individual Contour administrator may want to enforce for their cluster, but that are not explicitly required by Contour itself. +In this example, we'll define a constraint template and constraint to enforce that all `HTTPProxies` can be configured with at most five retries for any route. + +Create a file called `retry-count-range-template.yml` containing the following YAML: + +```yaml +apiVersion: templates.gatekeeper.sh/v1beta1 +kind: ConstraintTemplate +metadata: + name: httpproxyretrycountrange +spec: + crd: + spec: + names: + kind: HTTPProxyRetryCountRange + listKind: HTTPProxyRetryCountRangeList + plural: HTTPProxyRetryCountRanges + singular: HTTPProxyRetryCountRange + scope: Namespaced + validation: + openAPIV3Schema: + properties: + min: + type: integer + max: + type: integer + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package httpproxy.retrycountrange + + # build a set of all the retry count values + retry_counts[val] { + val := input.review.object.spec.routes[_].retryPolicy.count + } + + # is there a retry count value that's greater than the allowed max? + violation[{"msg": msg}] { + retry_counts[_] > input.parameters.max + msg := sprintf("retry count must be less than or equal to %v", [input.parameters.max]) + } + + # is there a retry count value that's less than the allowed min? + violation[{"msg": msg}] { + retry_counts[_] < input.parameters.min + msg := sprintf("retry count must be greater than or equal to %v", [input.parameters.min]) + } +``` + +Apply it to the cluster: + +```bash +$ kubectl apply -f retry-count-range-template.yml +``` + +Again, within a few seconds, you'll see that a corresponding CRD has been created in the cluster: + +```bash +$ kubectl get crd httpproxyretrycountrange.constraints.gatekeeper.sh +NAME CREATED AT +httpproxyretrycountrange.constraints.gatekeeper.sh 2020-08-13T16:12:10Z +``` + +Now, create a file called `retry-count-range-constraint.yml` containing the following YAML: + +```yaml +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: HTTPProxyRetryCountRange +metadata: + name: httpproxy-retry-count-range +spec: + match: + kinds: + - apiGroups: ["projectcontour.io"] + kinds: ["HTTPProxy"] + namespaces: + - my-namespace + parameters: + max: 5 +``` + +Note that for this `Constraint`, we've added a `spec.match.namespaces` field which defines that this policy should only be applied to `HTTPProxies` created in the `my-namespace` namespace. +If this `namespaces` matcher is not specified, then the `Constraint` applies to all namespaces. +You can read more about `Constraint` matchers on the [Gatekeeper website](https://github.com/open-policy-agent/gatekeeper#constraints). + +Apply it to the cluster: + +```bash +$ kubectl apply -f retry-count-range-constraint.yml +``` + +Now, let's create some `HTTPProxies` to see the policy in action. + +Create a namespace called `my-namespace`: + +```bash +$ kubectl create namespace my-namespace +namespace/my-namespace created +``` + +Create a file called `httpproxy-retries.yml` containing the following YAML: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: demo-retries + namespace: my-namespace +spec: + virtualhost: + fqdn: retries.projectcontour.io + routes: + - conditions: + - prefix: /foo + services: + - name: s1 + port: 80 + retryPolicy: + count: 6 +``` + +Apply the YAML: + +```bash +$ kubectl apply -f httpproxy-retries.yml +``` + +You should see something like: +``` +Error from server ([denied by httpproxy-retry-count-range] retry count must be less than or equal to 5): error when creating "proxy-retries.yml": admission webhook "validation.gatekeeper.sh" denied the request: [denied by httpproxy-retry-count-range] retry count must be less than or equal to 5 +``` + +Now, change the `count` field on the last line of `httpproxy-retries.yml` to have a value of `5`. Save the file, and apply it again: + +```bash +$ kubectl apply -f httpproxy-retries.yml +``` + +Now the `HTTPProxy` creates successfully*. + +_* Note that the HTTPProxy is still marked invalid by Contour after creation because the service `s1` does not exist, but that's outside the scope of this guide._ + +Finally, create a file called `httpproxy-retries-default.yml` containing the following YAML: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: demo-retries + namespace: default +spec: + virtualhost: + fqdn: default.retries.projectcontour.io + routes: + - conditions: + - prefix: /foo + services: + - name: s1 + port: 80 + retryPolicy: + count: 6 +``` + +Remember that our `Constraint` was defined to apply only to the `my-namespace` namespace, so it should not block the creation of this proxy, even though it has a retry policy count outside the allowed range. + +Apply the YAML: + +```bash +$ kubectl apply -f httpproxy-retries-default.yml +``` + +The `HTTPProxy` creates successfully. + +## Gatekeeper Audit + +We've seen how Gatekeeper constraints can enforce constraints when a user tries to create a new `HTTPProxy`. Now let's look at how constraints can be applied to pre-existing resources in the cluster. + +Gatekeeper has an audit functionality, that periodically (every `60s` by default) checks all existing resources against the relevant set of constraints. Any violations are reported in the `Constraint` custom resource's `status.violations` field. This allows an administrator to periodically review & correct any pre-existing misconfigurations, while not having to worry about breaking existing resources when rolling out a new or updated constraint. + +To try this out, let's revisit the previous example, and change our constraint to allow a maximum retry count of four. + +Edit `retry-count-range-constraint.yml` and change the `max` field to have a value of `4`. Save the file. + +Apply it to the cluster: + +```bash +$ kubectl apply -f retry-count-range-constraint.yml +``` + +We know that the `demo-retries` proxy has a route with a `retryPolicy.count` of `5`. This should now be invalid according to the updated constraint. + +Wait up to `60s` for the next periodic audit to finish, then run: + +```bash +$ kubectl describe httpproxyretrycountrange httpproxy-retry-count-range +``` + +You should see something like: + +``` +... +Status: + ... + Violations: + Enforcement Action: deny + Kind: HTTPProxy + Message: retry policy count must be less than or equal to 4 + Name: demo-retries + Namespace: my-namespace +``` + +However, our `HTTPProxy` remains in the cluster and can continue to route requests, and the user can remediate the proxy to bring it inline with the policy on their own timeline. + +## Next steps + +Contour has a [growing library](https://github.com/projectcontour/contour/tree/main/examples/gatekeeper) of Gatekeeper constraint templates and constraints, for both **validations** and **policies**. + +If you're using Gatekeeper, we recommend that you apply all of the **validations** we've defined, since these rules are already being checked internally by Contour and reported as status errors/invalid proxies. +Using the Gatekeeper constraints will only improve the user experience since users will get earlier feedback if their proxies are invalid. +The **validations** can be found in `examples/gatekeeper/validations`. + + +You should take more of a pick-and-choose approach to our sample **policies**, since every organization will have different policy needs. +Feel free to use any/all/none of them, and augment them with your own policies if applicable. +The sample **policies** can be found in `examples/gatekeeper/policies`. + +And of course, if you do develop any new constraints that you think may be useful for the broader Contour community, we welcome contributions! diff --git a/site/content/docs/1.29/guides/gateway-api.md b/site/content/docs/1.29/guides/gateway-api.md new file mode 100644 index 00000000000..4bcc3140c03 --- /dev/null +++ b/site/content/docs/1.29/guides/gateway-api.md @@ -0,0 +1,212 @@ +--- +title: Using Gateway API with Contour +--- + +This tutorial walks through an example of using [Gateway API][1] with Contour. +See the [Contour reference documentation][5] for more information on Contour's Gateway API support. + +### Prerequisites +The following prerequisites must be met before following this guide: + +- A working [Kubernetes][2] cluster. Refer to the [compatibility matrix][3] for cluster version requirements. +- The [kubectl][4] command-line tool, installed and configured to access your cluster. + +## Deploy Contour with Gateway API enabled + +First, deploy Contour with Gateway API enabled. +This can be done using either [static or dynamic provisioning][6]. + +### Option #1: Statically provisioned + +Create Gateway API CRDs: +```shell +$ kubectl apply -f {{< param github_raw_url>}}/{{< param branch >}}/examples/gateway/00-crds.yaml +``` + +Create a GatewayClass: +```shell +kubectl apply -f - <}}/quickstart/contour.yaml +``` +This command creates: + +- Namespace `projectcontour` to run Contour +- Contour CRDs +- Contour RBAC resources +- Contour Deployment / Service +- Envoy DaemonSet / Service +- Contour ConfigMap + +Update the Contour configmap to enable Gateway API processing by specifying a gateway, and restart Contour to pick up the config change: + +```shell +kubectl apply -f - <}}/quickstart/contour-gateway-provisioner.yaml +``` + +This command creates: + +- Namespace `projectcontour` to run the Gateway provisioner +- Contour CRDs +- Gateway API CRDs +- Gateway provisioner RBAC resources +- Gateway provisioner Deployment + +Create a GatewayClass: + +```shell +kubectl apply -f - <}}/{{< param branch >}}/examples/example-workload/gatewayapi/kuard/kuard.yaml +``` +This command creates: + +- A Deployment named `kuard` in the default namespace to run kuard as the test application. +- A Service named `kuard` in the default namespace to expose the kuard application on TCP port 80. +- An HTTPRoute named `kuard` in the default namespace, attached to the `contour` Gateway, to route requests for `local.projectcontour.io` to the kuard service. + +Verify the kuard resources are available: +```shell +$ kubectl get po,svc,httproute -l app=kuard +NAME READY STATUS RESTARTS AGE +pod/kuard-798585497b-78x6x 1/1 Running 0 21s +pod/kuard-798585497b-7gktg 1/1 Running 0 21s +pod/kuard-798585497b-zw42m 1/1 Running 0 21s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/kuard ClusterIP 172.30.168.168 80/TCP 21s + +NAME HOSTNAMES +httproute.gateway.networking.k8s.io/kuard ["local.projectcontour.io"] +``` + +## Test Routing + +_Note, for simplicity and compatibility across all platforms we'll use `kubectl port-forward` to get traffic to Envoy, but in a production environment you would typically use the Envoy service's address._ + +Port-forward from your local machine to the Envoy service: +```shell +# If using static provisioning +$ kubectl -n projectcontour port-forward service/envoy 8888:80 + +# If using dynamic provisioning +$ kubectl -n projectcontour port-forward service/envoy-contour 8888:80 +``` + +In another terminal, make a request to the application via the forwarded port (note, `local.projectcontour.io` is a public DNS record resolving to 127.0.0.1 to make use of the forwarded port): +```shell +$ curl -i http://local.projectcontour.io:8888 +``` +You should receive a 200 response code along with the HTML body of the main `kuard` page. + +You can also open http://local.projectcontour.io:8888/ in a browser. + +### Further reading + +This guide only scratches the surface of the Gateway API's capabilities. See the [Gateway API website][1] for more information. + + +[1]: https://gateway-api.sigs.k8s.io/ +[2]: https://kubernetes.io/ +[3]: https://projectcontour.io/resources/compatibility-matrix/ +[4]: https://kubernetes.io/docs/tasks/tools/install-kubectl/ +[5]: /docs/{{< param version >}}/config/gateway-api +[6]: /docs/{{< param version >}}/config/gateway-api#enabling-gateway-api-in-contour \ No newline at end of file diff --git a/site/content/docs/1.29/guides/global-rate-limiting.md b/site/content/docs/1.29/guides/global-rate-limiting.md new file mode 100644 index 00000000000..99a3c45d1bc --- /dev/null +++ b/site/content/docs/1.29/guides/global-rate-limiting.md @@ -0,0 +1,503 @@ +--- +title: Global Rate Limiting +--- + +Starting in version 1.13, Contour supports [Envoy global rate limiting][1]. +In global rate limiting, Envoy communicates with an external Rate Limit Service (RLS) over gRPC to make rate limit decisions for each request. +Envoy is configured to produce 1+ descriptors for incoming requests, containing things like the client IP, header values, and more. +Envoy sends descriptors to the RLS, and the RLS returns a rate limiting decision to Envoy based on the descriptors and the RLS's configured rate limits. + +In this guide, we'll walk through deploying an RLS, configuring it in Contour, and configuring an `HTTPProxy` to use it for rate limiting. + +**NOTE: you should not consider the RLS deployment in this guide to be production-ready.** +The instructions and example YAML below are intended to be a demonstration of functionality only. +Each user will have their own unique production requirements for their RLS deployment. + +## Prerequisites + +This guide assumes that you have: + +- A local KinD cluster created using [the Contour guide][2]. +- Contour installed and running in the cluster using the [quick start][3]. + +## Deploy an RLS + +For this guide, we'll deploy the [Envoy rate limit service][4] as our RLS. +Per the project's README: + +> The rate limit service is a Go/gRPC service designed to enable generic rate limit scenarios from different types of applications. +> Applications request a rate limit decision based on a domain and a set of descriptors. +> The service reads the configuration from disk via [runtime][10], composes a cache key, and talks to the Redis cache. +> A decision is then returned to the caller. + +However, any service that implements the [RateLimitService gRPC interface][5] is supported by Contour/Envoy. + +Create a config map with [the ratelimit service configuration][6]: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: ratelimit-config + namespace: projectcontour +data: + ratelimit-config.yaml: | + domain: contour + descriptors: + + # requests with a descriptor of ["generic_key": "foo"] + # are limited to one per minute. + - key: generic_key + value: foo + rate_limit: + unit: minute + requests_per_unit: 1 + + # each unique remote address (i.e. client IP) + # is limited to three requests per minute. + - key: remote_address + rate_limit: + unit: minute + requests_per_unit: 3 +``` + +Create a deployment for the RLS that mounts the config map as a volume. +**This configuration is for demonstration purposes only and is not a production-ready deployment.** +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: ratelimit + name: ratelimit + namespace: projectcontour +spec: + replicas: 1 + selector: + matchLabels: + app: ratelimit + template: + metadata: + labels: + app: ratelimit + spec: + containers: + - name: redis + image: redis:alpine + env: + - name: REDIS_SOCKET_TYPE + value: tcp + - name: REDIS_URL + value: redis:6379 + - name: ratelimit + image: docker.io/envoyproxy/ratelimit:19f2079f + ports: + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 8081 + name: grpc + protocol: TCP + volumeMounts: + - name: ratelimit-config + mountPath: /data/ratelimit/config + readOnly: true + env: + - name: USE_STATSD + value: "false" + - name: LOG_LEVEL + value: debug + - name: REDIS_SOCKET_TYPE + value: tcp + - name: REDIS_URL + value: localhost:6379 + - name: RUNTIME_ROOT + value: /data + - name: RUNTIME_SUBDIRECTORY + value: ratelimit + - name: RUNTIME_WATCH_ROOT + value: "false" + # need to set RUNTIME_IGNOREDOTFILES to true to avoid issues with + # how Kubernetes mounts configmaps into pods. + - name: RUNTIME_IGNOREDOTFILES + value: "true" + command: ["/bin/ratelimit"] + livenessProbe: + httpGet: + path: /healthcheck + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: ratelimit-config + configMap: + name: ratelimit-config +``` + +Create a service: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: ratelimit + namespace: projectcontour +spec: + ports: + - port: 8081 + name: grpc + protocol: TCP + selector: + app: ratelimit + type: ClusterIP +``` + +Check the progress of the deployment: + +```bash +$ kubectl -n projectcontour get pods -l app=ratelimit +NAME READY STATUS RESTARTS AGE +ratelimit-658f4b8f6b-2hnrf 2/2 Running 0 12s +``` + +Once the pod is `Running` with `2/2` containers ready, move onto the next step. + +## Configure the RLS with Contour + +Create a Contour extension service for the RLS: + +```yaml +apiVersion: projectcontour.io/v1alpha1 +kind: ExtensionService +metadata: + namespace: projectcontour + name: ratelimit +spec: + protocol: h2c + # The service name and port correspond to + # the service we created in the previous + # step. + services: + - name: ratelimit + port: 8081 + timeoutPolicy: + response: 100ms +``` + +Update the Contour configmap to have the following RLS configuration: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: contour + namespace: projectcontour +data: + contour.yaml: | + rateLimitService: + # extensionService is the / + # of the ExtensionService we created in the + # previous step. + extensionService: projectcontour/ratelimit + # domain corresponds to the domain in the + # projectcontour/ratelimit-config config map. + domain: contour + # failOpen is whether to allow requests through + # if there's an error connecting to the RLS. + failOpen: false +``` + +Restart Contour to pick up the new config map: + +```bash +$ kubectl -n projectcontour rollout restart deploy/contour +deployment.apps/contour restarted +``` + +## Deploy a sample app + +To demonstrate how to use global rate limiting in a `HTTPProxy` resource, we first need to deploy a simple echo application: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-conformance-echo +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: ingress-conformance-echo + template: + metadata: + labels: + app.kubernetes.io/name: ingress-conformance-echo + spec: + containers: + - name: conformance-echo + image: agervais/ingress-conformance-echo:latest + ports: + - name: http-api + containerPort: 3000 + readinessProbe: + httpGet: + path: /health + port: 3000 +--- +apiVersion: v1 +kind: Service +metadata: + name: ingress-conformance-echo +spec: + ports: + - name: http + port: 80 + targetPort: http-api + selector: + app.kubernetes.io/name: ingress-conformance-echo +``` + +This echo server will respond with a JSON object that reports information about the HTTP request it received, including the request headers. + +Once the application is running, we can expose it to Contour with a `HTTPProxy` resource: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: ingress-conformance-echo + port: 80 + - conditions: + - prefix: /foo + services: + - name: ingress-conformance-echo + port: 80 +``` + +We can verify that the application is working by requesting any path: + +```bash +$ curl -k http://local.projectcontour.io/test/$((RANDOM)) +{"TestId":"","Path":"/test/22808","Host":"local.projectcontour.io","Method":"GET","Proto":"HTTP/1.1","Headers":{"Accept":["*/*"],"Content-Length":["0"],"User-Agent":["curl/7.75.0"],"X-Envoy-Expected-Rq-Timeout-Ms":["15000"],"X-Envoy-Internal":["true"],"X-Forwarded-For":["172.18.0.1"],"X-Forwarded-Proto":["http"],"X-Request-Id":["8ecb85e1-271b-44b4-9cf0-4859cbaed7a7"],"X-Request-Start":["t=1612903866.309"]}} +``` + +## Add global rate limit policies + +Now that we have a working application exposed by a `HTTPProxy` resource, we can add global rate limiting to it. + +Edit the `HTTPProxy` that we created in the previous step to add rate limit policies to both routes: + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: ingress-conformance-echo + port: 80 + rateLimitPolicy: + global: + descriptors: + - entries: + - remoteAddress: {} + - conditions: + - prefix: /foo + services: + - name: ingress-conformance-echo + port: 80 + rateLimitPolicy: + global: + descriptors: + - entries: + - remoteAddress: {} + - entries: + - genericKey: + value: foo +``` + +## Default Global rate limit policy + +Contour supports defining a default global rate limit policy in the `rateLimitService` configuration +which is applied to all virtual hosts unless the host is opted-out by +explicitly setting `disabled` to `true`. This is useful for a single-tenant +setup use-case. This means you don't have to edit all HTTPProxy objects with the same rate limit policies, instead you can +define the policies in the `rateLimitService` configuration like this: +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: contour + namespace: projectcontour +data: + contour.yaml: | + rateLimitService: + extensionService: projectcontour/ratelimit + domain: contour + failOpen: false + defaultGlobalRateLimitPolicy: + descriptors: + - entries: + - requestHeader: + headerName: X-Custom-Header + descriptorKey: CustomHeader +``` + +Virtual host can opt out by setting `disabled` to `true`. +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + rateLimitPolicy: + global: + disabled: true + routes: + - conditions: + - prefix: / + services: + - name: ingress-conformance-echo + port: 80 +``` + +Also, the default global rate limit policy is not applied in case the virtual host defines its own global rate limit policy. +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: echo +spec: + virtualhost: + fqdn: local.projectcontour.io + rateLimitPolicy: + global: + descriptors: + - entries: + - remoteAddress: {} + routes: + - conditions: + - prefix: / + services: + - name: ingress-conformance-echo + port: 80 +``` + +## Make requests + +Before making requests to our `HTTPProxy`, let's quickly revisit the `ratelimit-config` config map. +Here's what we defined: + +```yaml +... +descriptors: + # requests with a descriptor of ["generic_key": "foo"] + # are limited to one per minute. + - key: generic_key + value: foo + rate_limit: + unit: minute + requests_per_unit: 1 + + # each unique remote address (i.e. client IP) + # is limited to three total requests per minute. + - key: remote_address + rate_limit: + unit: minute + requests_per_unit: 3 +``` + +The first entry says that requests with a descriptor of `["generic_key": "foo"]` should be limited to one per minute. +The second entry says that each unique remote address (client IP) should be allowed three total requests per minute. +All relevant rate limits are applied for each request, and requests that result in a `429 (Too Many Requests)` count against limits. + +So, we should be able to make: +- a first request to `local.projectcontour.io/foo` that get a `200 (OK)` response +- a second request to `local.projectcontour.io/foo` that gets a `429 (Too Many Requests)` response (due to the first rate limit) +- a third request to `local.projectcontour.io/bar`that gets a `200 (OK)` response +- a fourth request to `local.projectcontour.io/bar`that gets a `429 (Too Many Requests)` response (due to the second rate limit) + +Let's try it out (remember, you'll need to make all of these requests within 60 seconds since the rate limits are per minute): + +Request #1: +``` +$ curl -I local.projectcontour.io/foo + +HTTP/1.1 200 OK +content-type: application/json +date: Mon, 08 Feb 2021 22:25:06 GMT +content-length: 403 +x-envoy-upstream-service-time: 4 +vary: Accept-Encoding +server: envoy +``` + +Request #2: + +``` +$ curl -I local.projectcontour.io/foo + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Mon, 08 Feb 2021 22:59:10 GMT +server: envoy +transfer-encoding: chunked +``` + +Request #3: + +``` +$ curl -I local.projectcontour.io/bar + +HTTP/1.1 200 OK +content-type: application/json +date: Mon, 08 Feb 2021 22:59:54 GMT +content-length: 404 +x-envoy-upstream-service-time: 2 +vary: Accept-Encoding +server: envoy +``` + +Request #4: + +``` +$ curl -I local.projectcontour.io/bar + +HTTP/1.1 429 Too Many Requests +x-envoy-ratelimited: true +date: Mon, 08 Feb 2021 23:00:28 GMT +server: envoy +transfer-encoding: chunked +``` + +## Wrapping up + +For more information, see the [Contour rate limiting documentation][7] and the [API reference documentation][8]. + +The YAML used in this guide is available [in the Contour repository][9]. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting +[2]: ../deploy-options/#kind +[3]: https://projectcontour.io/getting-started/#option-1-quickstart +[4]: https://github.com/envoyproxy/ratelimit +[5]: https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/ratelimit/v3/rls.proto +[6]: https://github.com/envoyproxy/ratelimit#configuration +[7]: ../config/rate-limiting/ +[8]: ../config/api/ +[9]: {{< param github_url>}}/tree/main/examples/ratelimit +[10]: https://github.com/lyft/goruntime diff --git a/site/content/docs/1.29/guides/grpc.md b/site/content/docs/1.29/guides/grpc.md new file mode 100644 index 00000000000..12f0d9035fb --- /dev/null +++ b/site/content/docs/1.29/guides/grpc.md @@ -0,0 +1,225 @@ +--- +title: Configuring ingress to gRPC services with Contour +--- + +## Example gRPC Service + +The below examples use the [gRPC server][1] used in Contour end to end tests. +The server implements a service `yages.Echo` with two methods `Ping` and `Reverse`. +It also implements the [gRPC health checking service][2] (see [here][3] for more details) and is bundled with the [gRPC health probe][4]. + +An example base deployment and service for a gRPC server utilizing plaintext HTTP/2 are provided here: + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/name: grpc-echo + name: grpc-echo +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: grpc-echo + template: + metadata: + labels: + app.kubernetes.io/name: grpc-echo + spec: + containers: + - name: grpc-echo + image: ghcr.io/projectcontour/yages:v0.1.0 + ports: + - name: grpc + containerPort: 9000 + readinessProbe: + exec: + command: ["/grpc-health-probe", "-addr=localhost:9000"] +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: grpc-echo + name: grpc-echo +spec: + selector: + app.kubernetes.io/name: grpc-echo + ports: + - port: 9000 + protocol: TCP + targetPort: grpc +``` + +## HTTPProxy Configuration + +Configuring proxying to a gRPC service with HTTPProxy is as simple as specifying the protocol Envoy uses with the upstream application via the `spec.routes[].services[].protocol` field. +For example, in the resource below, for proxying plaintext gRPC to the `yages` sample app, the protocol is set to `h2c` to denote HTTP/2 over cleartext. +For TLS secured gRPC, the protocol used would be `h2`. + +Route path prefix matching can be used to match a specific gRPC message if required. + +```yaml +--- +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: my-grpc-service +spec: + virtualhost: + fqdn: my-grpc-service.foo.com + routes: + - conditions: + - prefix: /yages.Echo/Ping # Matches a specific gRPC method. + services: + - name: grpc-echo + port: 9000 + protocol: h2c + - conditions: + - prefix: / # Matches everything else. + services: + - name: grpc-echo + port: 9000 + protocol: h2c +``` + +Using the sample deployment above along with this HTTPProxy example, you can test calling this plaintext gRPC server with the following [grpcurl][5] command: + +``` +grpcurl -plaintext -authority=my-grpc-service.foo.com yages.Echo/Ping +``` + +If implementing a streaming RPC, it is likely you will need to adjust per-route timeouts to ensure streams are kept alive for the appropriate durations needed. +Relevant timeout fields to adjust include the HTTPProxy `spec.routes[].timeoutPolicy.response` field which defaults to 15s and should be increased as well as the global timeout policy configurations in the Contour configuration file `timeouts.request-timeout` and `timeouts.max-connection-duration`. + +## Ingress v1 Configuration + +To configure routing for gRPC requests with Ingress v1, you must add an annotation on the upstream Service resource as below. + +```yaml +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: grpc-echo + annotations: + projectcontour.io/upstream-protocol.h2c: "9000" + name: grpc-echo +spec: + selector: + app.kubernetes.io/name: grpc-echo + ports: + - port: 9000 + protocol: TCP + targetPort: grpc +``` + +The annotation key must follow the form `projectcontour.io/upstream-protocol.{protocol}` where `{protocol}` is `h2c` for plaintext gRPC or `h2` for TLS encrypted gRPC to the upstream application. +The annotation value contains a comma-separated list of port names and/or numbers that must match with the ones defined in the Service definition. + +Using the Service above with the Ingress resource below should achieve the same configuration as with an HTTPProxy. + +```yaml +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: my-grpc-service +spec: + rules: + - host: my-grpc-service.foo.com + http: + paths: + - path: / + backend: + service: + name: grpc-echo + port: + number: 9000 + pathType: Prefix +``` + +## Gateway API Configuration + +Gateway API now supports a specific resource [GRPCRoute][6] for routing gRPC requests. + +Configuring GRPCRoute for routing gRPC requests needs to specify parentRefs, hostnames, and routing rules with specific backendRefs. In the below example, route path matching is conducted via method matching rule for declared services and their methods. + +```yaml +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GRPCRoute +metadata: + name: yages +spec: + parentRefs: + - namespace: projectcontour + name: contour + hostnames: + - my-grpc-service.foo.com + rules: + - matches: + - method: + service: yages.Echo + method: Ping + - method: + service: grpc.reflection.v1alpha.ServerReflection + method: ServerReflectionInfo + backendRefs: + - name: grpc-echo + port: 9000 +``` +Using the sample deployment above along with this GRPCRoute example, you can test calling this plaintext gRPC server with the same grpcurl command: + +```yaml +grpcurl -plaintext -authority=my-grpc-service.foo.com yages.Echo/Ping +``` +Note that the second matching method for service of ServerReflection is required by grpcurl command. + +When using GRPCRoute, user should annotate their Service similarly to when using Ingress Configuration, to indicate the protocol to use when connecting to the backend Service, i.e. h2c for HTTP plaintext and h2 for TLS encrypted HTTPS. If it's not specified, Contour will infer the protocol based on the Gateway Listener protocol, h2c for HTTP and h2 for HTTPS. + + + + +## gRPC-Web + +Contour configures Envoy to automatically convert [gRPC-Web][7] HTTP/1 requests to gRPC over HTTP/2 RPC calls to an upstream service. +This is a convenience addition to make usage of gRPC web application client libraries and the like easier. + +Note that you still must provide configuration of the upstream protocol to have gRPC-Web requests converted to gRPC to the upstream app. +If your upstream application does not in fact support gRPC, you may get a protocol error. +In that case, please see [this issue][8]. + +For example, with the example deployment and routing configuration provided above, an example HTTP/1.1 request and response via `curl` looks like: + +``` +curl \ + -s -v \ + /yages.Echo/Ping \ + -XPOST \ + -H 'Host: my-grpc-service.foo.com' \ + -H 'Content-Type: application/grpc-web-text' \ + -H 'Accept: application/grpc-web-text' \ + -d'AAAAAAA=' +``` + +This `curl` command sends and receives gRPC messages as base 64 encoded text over HTTP/1.1. +Piping the output to `base64 -d | od -c` we can see the raw text gRPC response: + +``` +0000000 \0 \0 \0 \0 006 \n 004 p o n g 200 \0 \0 \0 036 +0000020 g r p c - s t a t u s : 0 \r \n g +0000040 r p c - m e s s a g e : \r \n +0000056 +``` + +[1]: https://github.com/projectcontour/yages +[2]: https://pkg.go.dev/google.golang.org/grpc/health/grpc_health_v1 +[3]: https://github.com/grpc/grpc/blob/master/doc/health-checking.md +[4]: https://github.com/grpc-ecosystem/grpc-health-probe +[5]: https://github.com/fullstorydev/grpcurl +[6]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRoute +[7]: https://github.com/grpc/grpc-web +[8]: https://github.com/projectcontour/contour/issues/4290 diff --git a/site/content/docs/1.29/guides/health-checking.md b/site/content/docs/1.29/guides/health-checking.md new file mode 100644 index 00000000000..8e7bcdb5bb5 --- /dev/null +++ b/site/content/docs/1.29/guides/health-checking.md @@ -0,0 +1,11 @@ +--- +title: Health Checking +--- + +Contour exposes two health endpoints `/health` and `/healthz`. By default these paths are serviced by `0.0.0.0:8000` and are configurable using the `--health-address` and `--health-port` flags. + +e.g. `--health-port 9999` would create a health listener of `0.0.0.0:9999` + +**Note:** the `Service` deployment manifest when installing Contour must be updated to represent the same port as the above configured flags. + +The health endpoints perform a connection to the Kubernetes cluster's API. diff --git a/site/content/docs/1.29/guides/kind.md b/site/content/docs/1.29/guides/kind.md new file mode 100644 index 00000000000..dcc374b70af --- /dev/null +++ b/site/content/docs/1.29/guides/kind.md @@ -0,0 +1,63 @@ +--- +title: Creating a Contour-compatible kind cluster +--- + +This guide walks through creating a kind (Kubernetes in Docker) cluster on your local machine that can be used for developing and testing Contour. + +# Prerequisites + +Download & install Docker and kind: + +- Docker [installation information](https://docs.docker.com/desktop/#download-and-install) +- kind [download and install instructions](https://kind.sigs.k8s.io/docs/user/quick-start/) + +# Kind configuration file + +Create a kind configuration file locally. +This file will instruct kind to create a cluster with one control plane node and one worker node, and to map ports 80 and 443 on your local machine to ports 80 and 443 on the worker node container. +This will allow us to easily get traffic to Contour/Envoy running inside the kind cluster from our local machine. + +Copy the text below into the local yaml file `kind-config.yaml`: + +```yaml +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane +- role: worker + extraPortMappings: + - containerPort: 80 + hostPort: 80 + listenAddress: "0.0.0.0" + - containerPort: 443 + hostPort: 443 + listenAddress: "0.0.0.0" +``` + +# Kubernetes cluster using kind + +Create a kind cluster using the config file from above: + +```yaml +$ kind create cluster --config kind-config.yaml +``` + +Verify the nodes are ready by running: + +```yaml +$ kubectl get nodes +``` + +You should see 2 nodes listed with status **Ready**: +- kind-control-plane +- kind-worker + +Congratulations, you have created your cluster environment. You're ready to install Contour. + +_Note:_ When you are done with the cluster, you can delete it by running: +```yaml +$ kind delete cluster +``` + +# Next Steps +See https://projectcontour.io/getting-started/ for how to install Contour into your kind cluster. diff --git a/site/content/docs/1.29/guides/metrics/table.md b/site/content/docs/1.29/guides/metrics/table.md new file mode 100644 index 00000000000..89405d815c6 --- /dev/null +++ b/site/content/docs/1.29/guides/metrics/table.md @@ -0,0 +1,20 @@ +| Name | Type | Labels | Description | +| ---- | ---- | ------ | ----------- | +| contour_build_info | [GAUGE](https://prometheus.io/docs/concepts/metric_types/#gauge) | branch, revision, version | Build information for Contour. Labels include the branch and git SHA that Contour was built from, and the Contour version. | +| contour_cachehandler_onupdate_duration_seconds | [SUMMARY](https://prometheus.io/docs/concepts/metric_types/#summary) | | Histogram for the runtime of xDS cache regeneration. | +| contour_dag_cache_object | [GAUGE](https://prometheus.io/docs/concepts/metric_types/#gauge) | kind | Total number of items that are currently in the DAG cache. | +| contour_dagrebuild_seconds | [SUMMARY](https://prometheus.io/docs/concepts/metric_types/#summary) | | Duration in seconds of DAG rebuilds | +| contour_dagrebuild_timestamp | [GAUGE](https://prometheus.io/docs/concepts/metric_types/#gauge) | | Timestamp of the last DAG rebuild. | +| contour_dagrebuild_total | [COUNTER](https://prometheus.io/docs/concepts/metric_types/#counter) | | Total number of times DAG has been rebuilt since startup | +| contour_eventhandler_operation_total | [COUNTER](https://prometheus.io/docs/concepts/metric_types/#counter) | kind, op | Total number of Kubernetes object changes Contour has received by operation and object kind. | +| contour_httpproxy | [GAUGE](https://prometheus.io/docs/concepts/metric_types/#gauge) | namespace | Total number of HTTPProxies that exist regardless of status. | +| contour_httpproxy_invalid | [GAUGE](https://prometheus.io/docs/concepts/metric_types/#gauge) | namespace, vhost | Total number of invalid HTTPProxies. | +| contour_httpproxy_orphaned | [GAUGE](https://prometheus.io/docs/concepts/metric_types/#gauge) | namespace | Total number of orphaned HTTPProxies which have no root delegating to them. | +| contour_httpproxy_root | [GAUGE](https://prometheus.io/docs/concepts/metric_types/#gauge) | namespace | Total number of root HTTPProxies. Note there will only be a single root HTTPProxy per vhost. | +| contour_httpproxy_valid | [GAUGE](https://prometheus.io/docs/concepts/metric_types/#gauge) | namespace, vhost | Total number of valid HTTPProxies. | +| contour_status_update_conflict_total | [COUNTER](https://prometheus.io/docs/concepts/metric_types/#counter) | kind | Number of status update conflicts encountered by object kind. | +| contour_status_update_duration_seconds | [SUMMARY](https://prometheus.io/docs/concepts/metric_types/#summary) | error, kind | How long a status update takes to finish. | +| contour_status_update_failed_total | [COUNTER](https://prometheus.io/docs/concepts/metric_types/#counter) | kind | Number of status updates that failed by object kind. | +| contour_status_update_noop_total | [COUNTER](https://prometheus.io/docs/concepts/metric_types/#counter) | kind | Number of status updates that are no-ops by object kind. This is a subset of successful status updates. | +| contour_status_update_success_total | [COUNTER](https://prometheus.io/docs/concepts/metric_types/#counter) | kind | Number of status updates that succeeded by object kind. | +| contour_status_update_total | [COUNTER](https://prometheus.io/docs/concepts/metric_types/#counter) | kind | Total number of status updates by object kind. | diff --git a/site/content/docs/1.29/guides/prometheus.md b/site/content/docs/1.29/guides/prometheus.md new file mode 100644 index 00000000000..f0b7364c340 --- /dev/null +++ b/site/content/docs/1.29/guides/prometheus.md @@ -0,0 +1,94 @@ +--- +title: Collecting Metrics with Prometheus +--- + + + +Contour and Envoy expose metrics that can be scraped with Prometheus. By +default, annotations to gather them are in all the `deployment` yamls and they +should work out of the box with most configurations. + +## Envoy Metrics + +Envoy typically [exposes metrics](https://www.envoyproxy.io/docs/envoy/v1.15.0/configuration/http/http_conn_man/stats#config-http-conn-man-stats) through an endpoint on its admin interface. To +avoid exposing the entire admin interface to Prometheus (and other workloads in +the cluster), Contour configures a static listener that sends traffic to the +stats endpoint and nowhere else. + +Envoy supports Prometheus-compatible `/stats/prometheus` endpoint for metrics on +port `8002`. + +## Contour Metrics + +Contour exposes a Prometheus-compatible `/metrics` endpoint that defaults to listening on port 8000. This can be configured by using the `--http-address` and `--http-port` flags for the `serve` command. + +**Note:** the `Service` deployment manifest when installing Contour must be updated to represent the same port as the configured flag. + +**The metrics endpoint exposes the following metrics:** + +{{% metrics-table %}} + +## Sample Deployment + +In the `/examples` directory there are example deployment files that can be used to spin up an example environment. +All deployments there are configured with annotations for prometheus to scrape by default, so it should be possible to utilize any of them with the following quickstart example instructions. + +### Deploy Prometheus + +A sample deployment of Prometheus and Alertmanager is provided that uses temporary storage. This deployment can be used for testing and development, but might not be suitable for all environments. + +#### Stateful Deployment + + A stateful deployment of Prometheus should use persistent storage with [Persistent Volumes and Persistent Volume Claims][1] to maintain a correlation between a data volume and the Prometheus Pod. + Persistent volumes can be static or dynamic and depends on the backend storage implementation utilized in environment in which the cluster is deployed. For more information, see the [Kubernetes documentation on types of persistent volumes][2]. + +#### Quick start + +```sh +# Deploy +$ kubectl apply -f examples/prometheus +``` + +#### Access the Prometheus web UI + +```sh +$ kubectl -n projectcontour-monitoring port-forward $(kubectl -n projectcontour-monitoring get pods -l app=prometheus -l component=server -o jsonpath='{.items[0].metadata.name}') 9090:9090 +``` + +then go to `http://localhost:9090` in your browser. + +#### Access the Alertmanager web UI + +```sh +$ kubectl -n projectcontour-monitoring port-forward $(kubectl -n projectcontour-monitoring get pods -l app=prometheus -l component=alertmanager -o jsonpath='{.items[0].metadata.name}') 9093:9093 +``` + +then go to `http://localhost:9093` in your browser. + +### Deploy Grafana + +A sample deployment of Grafana is provided that uses temporary storage. + +#### Quick start + +```sh +# Deploy +$ kubectl apply -f examples/grafana/ + +# Create secret with grafana credentials +$ kubectl create secret generic grafana -n projectcontour-monitoring \ + --from-literal=grafana-admin-password=admin \ + --from-literal=grafana-admin-user=admin +``` + +#### Access the Grafana UI + +```sh +$ kubectl port-forward $(kubectl get pods -l app=grafana -n projectcontour-monitoring -o jsonpath='{.items[0].metadata.name}') 3000 -n projectcontour-monitoring +``` + +then go to `http://localhost:3000` in your browser. +The username and password are from when you defined the Grafana secret in the previous step. + +[1]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/ +[2]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes \ No newline at end of file diff --git a/site/content/docs/1.29/guides/proxy-proto.md b/site/content/docs/1.29/guides/proxy-proto.md new file mode 100644 index 00000000000..7753d8c5776 --- /dev/null +++ b/site/content/docs/1.29/guides/proxy-proto.md @@ -0,0 +1,53 @@ +--- +title: How to Configure PROXY v1/v2 Support +--- + +If you deploy Contour as a Deployment or Daemonset, you will likely use a `type: LoadBalancer` Service to request an [external load balancer][1] from your hosting provider. +If you use the Elastic Load Balancer (ELB) service from Amazon's EC2, you need to perform a couple of additional steps to enable the [PROXY][0] protocol. Here's why: + +External load balancers typically operate in one of two modes: a layer 7 HTTP proxy, or a layer 4 TCP proxy. +The former cannot be used to load balance TLS traffic, because your cloud provider attempts HTTP negotiation on port 443. +So the latter must be used when Contour handles HTTP and HTTPS traffic. + +However this leads to a situation where the remote IP address of the client is reported as the inside address of your cloud provider's load balancer. +To rectify the situation, you can add annotations to your service and flags to your Contour Deployment or DaemonSet to enable the [PROXY][0] protocol which forwards the original client IP details to Envoy. + +## Enable PROXY protocol on your service in GKE + +In GKE clusters a `type: LoadBalancer` Service is provisioned as a Network Load Balancer and will forward traffic to your Envoy instances with their client addresses intact. +Your services should see the addresses in the `X-Forwarded-For` or `X-Envoy-External-Address` headers without having to enable a PROXY protocol. + +## Enable PROXY protocol on your service in AWS + +To instruct EC2 to place the ELB into `tcp`+`PROXY` mode, add the following annotations to the `contour` Service: + +``` +apiVersion: v1 +kind: Service +metadata: + annotations: + service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp + service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*' + name: contour + namespace: projectcontour +spec: + type: LoadBalancer +... +``` + +## Enable PROXY protocol support for all Envoy listening ports + +``` +... +spec: + containers: + - image: ghcr.io/projectcontour/contour: + imagePullPolicy: Always + name: contour + command: ["contour"] + args: ["serve", "--incluster", "--use-proxy-protocol"] +... +``` + +[0]: http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt +[1]: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer \ No newline at end of file diff --git a/site/content/docs/1.29/guides/resource-limits.md b/site/content/docs/1.29/guides/resource-limits.md new file mode 100644 index 00000000000..a531221a88f --- /dev/null +++ b/site/content/docs/1.29/guides/resource-limits.md @@ -0,0 +1,161 @@ +--- +title: Contour / Envoy Resource Limits +--- + +## Performance Testing Contour / Envoy + +- Cluster Specs + - Kubernetes + - Version: v1.12.6 + - Nodes: + - 5 Worker Nodes + - 2 CPUs Per Node + - 8 GB RAM Per Node + - 10 GB Network + - Contour + - Single Instance + - 4 Instances of Envoy running in a Daemonset + - Each instance of Envoy is running with HostNetwork + - Cluster Network Bandwidth + +Having a good understanding of the available bandwidth is key when it comes to analyzing performance. It will give you a sense of how many requests per second you can expect to push through the network you are working with. + +Use iperf3 to figure out the bandwidth available between two of the kubernetes nodes. The following will deploy an iperf3 server on one node, and an iperf3 client on another node: + +```bash +[ ID] Interval Transfer Bandwidth Retr +[ 4] 0.00-60.00 sec 34.7 GBytes 4.96 Gbits/sec 479 sender +[ 4] 0.00-60.00 sec 34.7 GBytes 4.96 Gbits/sec receiver +``` + +## Memory / CPU usage + +Verify the Memory & CPU usage with varying numbers of services, IngressRoute resources, and traffic load into the cluster. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Test CriteriaContourEnvoy
#Svc#IngRPSCCMemory (MB)CPU% / CoreMemory (MB)CPU% / Core
0000100150
5k000462%150%
10k000773%2052%
05k00361%2302%
010k00631%101%
5k5k002441%2211%
10k10k0026006%4304%
0030k60081%173%
00100k10k101%11814%
00200k20k91%19131%
00300k30k101%22540%
diff --git a/site/content/docs/1.29/img/archoverview.png b/site/content/docs/1.29/img/archoverview.png new file mode 100644 index 0000000000000000000000000000000000000000..f79bbfe1b4b3a7f0de886d7ce059722308be3ab4 GIT binary patch literal 78807 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 RG6D=h;OXk;vd$@?2>^o>9Z3KH literal 0 HcmV?d00001 diff --git a/site/content/docs/1.29/img/contour_deployment_in_k8s.png b/site/content/docs/1.29/img/contour_deployment_in_k8s.png new file mode 100644 index 0000000000000000000000000000000000000000..add5e554a07e9500b9c48681e476246cdc52d035 GIT binary patch literal 134492 zcmeFZXH-+$x<9Odh@wKIOErkni&CVclpqR75u_t*K)Mj=0gSpOh9*kyg7hZ67o|#< zF1>dMq4)eJ_v~}-HSB%AydT~#cMQjH4c1z7J@YBQr_Q{8tOC1y@!G{RXU<%HpeUz) z=FGWahD~vfCLJ&YWR6^FZ#NrmNxd$b|v}GdJn+ouRqW z)(bZty-r23lau>G_^$|YYLJZg3{hW4rBWhMlvG(o3i9=zRglJMgl2iyvb>SxbUm8L z8;{ZQibM|VoVUp$FfX=ujOOltAVAhg?hN0Nu*k|(Tpb@tV93)Ridfb-@o+lCCTU6 z&wg`-`RnEXEkP_Sv^$snhEVXj4D13)gSgdjB-`JU^sU4Ke|vv@R7^>Re^hO%kNfXQ zB59B${Ci?L)d*f=1FjXDU-(;+WcXL<{%f!Ick~_FAp;lBO^0SrxvB8nQ6m2`+L22?4q{#V#Qs?BeW+}0JvQ;kl~klDa&;VSqIfv zwl<>$&CTitR&$pcsLcd~)pOm1lG<3w9tG$>Y8O>~Q0N^H&cJaKHvJ>0yM!UPN-;ON z)y<$eSC}S5NWQQlQJ1p#33QCw49DCuznDQi;%QoLGD3*0*{`>VtE^&#Y{|WnfnOwP zh=TK`L-A2jP&0m8c-N_TOe)#hb7$a~R_Ny3=mU<%hb`>eNW`p!~vNtl z;?Nih7Jdc-|9zUF7d>{$r#uDHf~B*&`;OJjmz7(7t1B_;3Jl?_ZD>jlSohEvmwyDPb+&gPRg-L5Zz3s zv8Wr`kXedL$ZyvjQc_M0IfI-fUu$rMx89?|_uj}dQU>?U^EVr6(&xg=X4*v8Gf@RF zIi@E&{@1FFBcKLq$p}Nu^_ji)<|AL~wk2Bxo%0Qu_$TLxrn1CDoha}A{-i{$B~Y^W>8sOxkf+swD*I* z?fq9WEkIfIoL^qNg|DoKI`Kf0E>tE?>GZM(_%&hB(Z%nB#W)T)H0t;q>#C=-505Hc z2KIUF0}t#;&Jxg5%zyC~b-#EjhOlM-(0xy`(f*S{(i`YZBD8B1B2adje&kE2qnx+W zUUV?m23pP38$KZ-wX}j&{s@O4+fQPgz6vM&7^fhp#*YqO-eA8o4u>xu$S65Hm3w5B zxc#n6NeDU;u7PlR*-TpH-A=c2#Y|x5Dodvqh_be$(gN^^LS7+H_55aDl~V>|OCpo6 zI|~uWCLHuC@H_fOb-N2%KXydt{7FEBcu~{-w>eiKv#)Li)M-`rEfuwXVz7{avd@=tM zrkc4OD9-PAd|%FH?AeNRxAOt(9F7~k-fh}P)6e~s-+SwhL6avDNr4ZD<0NNON%5>R zo{-4A%ClW7p*wzhE<36;+)p1f_Qi0kypriA0bp-V zc@d!7f57a&E+lU7Z>))aLTgIigjVuFizaUS5r`5)XV`+u0`y#0we1)hVz`xG$xr}B zMm?uhI<)|UW2?Yav(Sj7=p+S4Jsc${!y<3r?%*J_Kd0|Ls^e`{6Hvq=WBe8<6|2OV zA$+AG5+-Sl5ic&;?%!DsbKLvIU|Ebx?X(Q9iKN8xm2#^ZxPFV+5wX#xhp_NN0UsgH ziaGE!TxJ|gaK3VDcaBQFwFPn@Uq`EOnZ}g`di+eeUu#Ssu|2$Xd2HkzVB;fTBK%fh zDLgI+^M1uG|C)l$w2j7TNbx$_)LY3BH7hziq+8gPpNl=7f~3_XgZJgY`{d?Pr#yD7 zzHwg=eA_=yOd4AGp%-;=Q+~Pyu3fS5F@J7MAR%NR`V8_J1-Ty?eb%3NN^q7S>gJ24 zXN@*GT4mDA5c~J23sKHt8Sm7LpUCZZm5i>c@;zOvcd9K3Ou9f$d;RRUU6OA@`1Zi^ zm?B&h-ac=lBz03=iVYg8Y&LyPT`q#vE3C!!fQ=>^+Z~jncn)B)9>8RKwWU)Wh<#V{ z4aR`$ik{j;4REQyl=Rm#5UU_qf_Ci?E4akDF7#Ksj;-05IS~+D;{irlP;!K45N8R< zNGq_ZJ9eY}W4PR46KAVp#z*%MV06itvLj{1;9^-TmW`pzn2RZQliS2Bq}m;9-Vnep|21-iW= zYM%cyu^cA^y%K=ReH^L5B0sFB;ErRPi#cwa&3ycXh(v+{F!7EW{8UN67)5ZN(d+OV zM?DRzNy>HJne7Rxd4EdjWrHzX5f^pz*7#oxI6_T77M?Q}5$BH<@8{8ee{*{pa-$E> z?mT(FgZNnjWtMTHDrJ5mHhle$VdY<1n+gn|EPSz58q@7W|1#2)MV*@`{m8!T1dF?g z87+}AFj5d)yB|j4yYOoq|3V=4j`z?jehi89rNxzt&=)S#>lm6??^!n|oi->DiC_3D zV?^D&?IS$=dih#Lwc~-Qc0hZSOVXB}oX#(Cb;Mn$^YYu#p|6cjkOf@Ug0AQ0WPXR? z*t;Xk_cD!udE(|h*YJxd*VrC=+(Eo_F;nt3DwSX5bP3R}x)HL#7BtBQ$$cWHdF_d> z=OWJsJ73LK-4k5Q+-dG(m5Fx%nAD&%^!|5LC4UY_iZHqzQungQia%~UpyW_(;aGu) zI{P+F4OMKZSYcv=(cUGy!*0022g*w!GUWhXhint}@!|Cv?^;7*z+gp=_jkBDCmBl7 z9;N7Uv6f!yoq}zNFbr`3EbDHW8vUTp8RR}-@RGfiJf2#_%M!V}2Sk0X${48PZtj;? z@{z0Kf|{uN6pW?WhKjD^ru8Fo=MUYu#HsF$5$S@gWOR{2QyR~>_^%QyjQ_&8sn8MT zJQQt12OH9es?xo`zbpzRD5l|PtC}C~_LhK_4*@N^4hZpi^c-&mnfOmc=9>&6&%Vz$ zlQy#lK7#nb>V#u@stwPdTUF(VbHgY2Uu_bZ+q*2IsM{a)2XnOEB{RXyD) zYZ4HLR*ONE8IZSaMAr$dp-H6Fu^#&ZtVU=M+PU5YQn5&wWx{7gB9VD(%#hG_@s1SV z<>^>ThnHU}OyxAiE&{Wmo)1*imh{jcJGO{%(vrSLK%}b*c;S9WLjZ3loia!|7^9H+ zVNFSx0%ria$=;7aPw0h*o={GWeOcLtA2R}ejEO3i3xTkF6#yBK#izU6Vh_NF9m_SV%l=RnAIglsA*|&VtpRRW%$+?D`UJm zTv}gPJX~r)S$`gCVlKD)QCYRw^jtc`K^46@q_`&%)Scsqj#6*-p6A;b`Q#46IedG~ zmtCvy&pD*RW8=Q%o#oiXePolvbM$}>(ZgJ)C6ZtW);ORp2HHiTERbmh2xb0^UvxyelYppNV8mO0p%kx;iNBnL-?l z<@{_Dx`Ml;2#z4w4Adqjs2MXV;5^>9)Gn5>Z6O6Bkf%T+jb9Am`|q|}{P`B`TJ`hm z(+t>c%3P<7C#ea-6i?}bc>SP9dfrTx(|K^|>g2Zl?kmV-8gf68BNU|~9+X~6ky#-L z8OvD}ieP(%T`2egidm`6(nNx`{r26^XK!V$}RuJK?$opYmul%rpK*)9>^cqs#VB!DQzpf_!skTyfA;9Mq)W zJQa4Ck>@dKW6p!1W9;OHiqY`TtC6=l&w$Spez3iF`t#R)81v0^7Nmrr)k08Zo2rXX zdXNMetX=YITGDxIukUzwBaf^jWNJaJ`f_3s1Rq%*5uERkTKy2+eG59t)}|SzD`q%8 z%7FDBF5iaxQX@821NzU%7>9$mYl*D!OXTrx=geEfhlD16xp+&^U|oVtl{U1}^R7^p z@W4b}5E$V(zNh@f&UI)I`Z!UO`}NcHIO5yK1}VdfI)Nc)`5ih4_8p2nvf6~oIamfg ziYgIHntVeOcLx(0uxOA^bnOPv0rCP{d>xpDgo_@Gp{}I;lw8(~m5e z35XT}={Xc%%$(p{@ry-0yA11@pc;>h(##_0>2JdYUEpmf* z@1)M)zdK8PcOEr$frR-}LcpV9XT&NF)rM~EgB!|LX#IrY+rybQBK?GGl3+bhnV zJ4+x70ETR&^CG@(`I2mIwlWqvZ%SIgrmM3UhlinNT%Tm78LCw|R1924=`1EdrZ`V< zeBOBeofaOOh;-$~mGc8f%J?`f|ro!JW zHN0`^O>Eq!;e!6iMmeGu`PzZCX}XSR@alE1p?9a%Ry)teaF=WT<-$) z{w;n@kjFeeH=>GfL%}tfzZ$&-94qcXZx(1ypxMZCp)=ud?4HFT3GqKu>wAIWWt{`F zGUS%r7aqOLmoryOgqjh}-$`e2=Pv^00!?C$j4Q{FMW_MhqbO9k;ZmYWt}4DE+4=<4 z0l`uQYs1|%@j#rI=xCp-PnGoEuh+8XzuBWgzj`C=gIbJxd(B&E1uC*0wuO`bF_!OZK{QShOTA;EH*t7_tnc2TR|dL+~XMLd3LB(Ebu3XVa_Xlu`KR zoMSr&m2}!@)D_)==Z5y@A3l$fF#t3>Ju3iJ#^1E4Q;8ryz;!2+a)Cm$D>h5BSXW#( zo=;x?CDzU2@Cl6Td8|y#$(yeWF5$l^MsSBLF5fe#IY+MUbAX0rOsSJ=5}5)T4uahV z53%8Uz`~)x!bub5@s(cdYPh(Iox@6&OPjwUCIH2wm*!mPoLR#Rozx)h5T9xdpz6Gf z6axw263UX}cZy}=orugUw@O4{L9+^VzIrKEAtyLoa$TDf#&=kpqu-xKynpktDgbdw zp)7fP1@x=AXD@l|CeP-1bMEjRinjaqpIikL=k}ppWQ|*)KEMgl2nYE6dltak=@!9! zgiIv>n=R@>CVUof5?J;@8ky-Km_XEAvC_}QWkqB;{L3D|IQJF%ySu=doCkC;lJnqu zpn4GAN~qa^+ge~HqQ4g-|uB)dGSzb&yyxj9G7rq{)sQqHNZc6l6+-+{+UzG zFD#^GG8S${D=vIN9S)mkswv0te%Obu2XYZEFwz_!EEKzxy*s|p^AG?#--9Lmw3vtE zMd!q=in~b&-GBi;`Ip6iP@wHBkmAtEhbIEKvoic;ltfS$L_yCT4)DE#GTVQ3Ft+A- zR5=sEDIgto>64V*3j{|TUA<-jqG8eA`+Q=IvN)ne4j})O{5hWJ)8b z%bImPHPq(*&lUC;E&+&!-O+MYgRjQ-3K{8pD3C#*j2#$U;fZhA$&he|9yQa3oj`Fh zbum$>vrXi8%B~*-gh5k-O*7Faby|a4vhy)DFM!SX$o5A5ruAT6q2)rW1fqRgUM-eE zlo=9KWK&8^Ic!I;lN{ZCZK!x=_#El%cA*}8|2lsc0EwX3S4V$Cg3!Uh2wVqoBT0}8 zdQ(vO*~JAxG*`%^EKTek%)W}XVKvdU>p+Tc^CVAk0dm)M+aZS&?qJot7ZQ|^ zVRljbc@)VsLy#90hvy$TG&XycptBch+Z8TMA{YW3ZCb-;T6}AuBf26s`L_5=6LTOc zt?hB(H8J1<7)2+!3`V9@tJmn`40`XySck~S0v*0QQ*^2Xf05wMTq~VHY4v{g?ZzJ* zNsPLm&4r5otO3FlHPdz*WV7g~PUI2mB0}Dz007+NNO}$F&g!#k8E5lk!Jjm(l;)5UHS+}2`jH;@t zSzAe#QAGN}019C&_lPM%X?ZXiWZ~d!{6Sn}p99{c`N(3YQ@TS*xC_s}g?JdX09COQxYFM+ayNYd(A9xQc4?Cl|aK-Vl`-zf~HMal+BxtnAMc-j?8J1faYQU@7b zg%o}`EQL%w6r;cM&U}Ck79bQf`3`ls!Qfy937HwaYX9zMDzYXHm_#h`@B0MM5OeN| zYvTirGLHl2CXUMZ)|pX`Mmx|xfOZu@a^2)K7b}ux6tq}i(@q?dJCKD2&}o^Af6lkuJ)Bw4xhFy`+Uuek{p7diWb z?KcJf58!l40mI*pNK*Ox#f>Sz;!&@rLCpSl4?JfE4*j52{np>!W%+smEUql$2qN#_ z9{_oAS2o`mrU2!NOYBz_l@9kcao}w6Hmq7kbNN_e-tmZ=Huz7FAHnb$M{0E2lEbK)m6 zV%jV#WgS&Us9+VprheEL4^~GA1XT0hLvp3k7hX~I8*X=N-5fOAKh9AU^}Yr5w5acD zIX=#$@2mi^TISA=X~`2PCQ&W|8z zf%)jkW_HtKv{{%@-q0rDsBYT~4Qm3QyzU2$y*AC{Ik~!98fdPAn=X}@K6zhvpR(!$ z&}VW3sYO$V4ComBCVrW@i~1p9P2x*DK#uwY2*4`Dn($CQH}_U`n;C0 z!Dcz}YDBd*HKfq^a7dNXPddH$TjpYY_s&>pdHJXL;E62Uq)3=Ce)~ zLM+|n5Kj8IN~4UUigzWoesrb{S@fPyRb~Vr*si8rL3#af4Y99^Ga~l7j`iottM*^n zYIsZCQr9m-{4k5l8QGcCZi|$7yduuwxGZ6pAz)v)kt7?}$BI0D#kt$(1v&ylqX4agq9$+C~_c71U`fY0bxv5!H z4juu!57;J`M7w0@_Au(rBFj}r({6>uE!O$(oeXjUgGYljD!8GmEgV(9p73;(JNyb< zUGDMG0nLpEz`-eXt8?KYN2eWuDy<)ef66~9aK+##yJ?}IX`z{_h1Wy*B-Yu(FiWKR z9pW78Y~|}hM3W?0=jOsVIx5|MP{q+pT=~P(OhL{;XC-<$)!>t9OR!1cH+UJVRH4bj zOq|<EwGH6$Z`L|&zMI$>8ut!^jO$Z0{j;|A@RWn9FN3v6pzaj)`?#aGzjv#)jS91Rd_i= zB7!D^qq{@WP~xnOu@JW$)XVnt0{8YWwAG;P&8!~qhILXTsOx(kd-ig=R-)hTi!ZKf z0lS#ssXX?s@6eQ$mT2q7GJT;cT-A<&%ZO9&NHS(eL^MTpI=XQt*ee2RPXXaWq4>f= za#KbTy?+W>gUVavN&UL6R_z+T0JaI}s}T9r>!WlWlH{WO>yb24!*2GP>(<)(PW4@q z+hg8{vF5{;rk^6tn*A*~xL*k#&QV@9)3>#06u!{NizY=p7yIbdsi}BZ-oZoZq7{xC2F2*6=~KU z*BX?Qd2TAB(WcN;Ez9eM<1&*x0#X051vl27Gi*PkBvg4LeNVL|=V-&>u4->e9GH@G z1dtPyUwj9T;WEau{9YzX*rDj|MU;dv)H%KKg28))%*t-y;MqQL+|02!rRQEK>aaH2 z%HFtta@bK5y&Hvk`843LfqjDytz@PXAYoVkQQOW)+^E92=fR^2Z*!G}=oAXS9L|3K z7WsY)Xj1=d!d(u><8E1xS(+qNw?^G-AQtZf-)*UJHhDF}P$;pwQp%iurE@%&*G2}H z(NX#3xv2gPKP-UnnI$y4lw)^S3$Od@Mt0>W0aM@JqY-gA6>&<@07WyVPYH(yQK3iA z_jnH&)b)Ozrg4J;^R*t3P1Y&VF9TA6NE!n%H#;Le)V?^70n^CBvfQaL8$o_^; zn2}4pEtBd0(&3XL@Rm|yo2d-tE1jGhvLCx+KDkY%PXp=TD=SYP+02(Ogx7WVR9*>> z4wZDBO`_KKtj`ZMl7gOW$+Ljyx zMB^IKlp7`?wPGhZ!`pS`-JsQ6M&Lr$vflJ$oa3d~qsoa->H!aZgH^9dSRhEg?GCQ; z7*U9NOThMgmDKhguiTB;c4Kh59}fqiSIiyj82l;uwp(kvcN*r}d6JLU7HgZEaG{O} zYv9hUjS3R|PhkLg2krY*4aLh`tlDRoI8v&Wf(*%nq)QPCGe(R7 z#u7;t+G-6R_7$`1xIyFG4Xzyz9W{Sd`Du$e_Nr^5+1YZDn#t?o!KRLqud%%$Jcb68 zg?tN-s-0pgnbx!$eS>YQed0dYJp|~MH zVT*r73@hOr2l7JgzQNT+CO@c{UGLK3yyWytfJZ76i`~rUu<(;s0|CdrS56?qhGQLW zbFAB`C##ruF-Gj@RVAEBLO4k`&=_G5S>cggCoTVlLv$wa*l~f%H>EvX;Cjx|sIUT6 zhW6odmrp=6_PR`b8p!gLC7J5+t5~+YYj3}~u3TO=z`4v!l(JU`{S_nXl1BHxRFx8z z-&a?1fUo#+!`q`^%`n)PD$SL#O8b`F9T(}R$4;6p`EWFj$MM)XU~^ef&0`5M^UnCN zJ||MhEq-w4eUrl@xqW$b2`5ad#2L}@&KU74&r=IiwDXZ>4r&z58W^ZJ<{ykfG30p8 zx7^#XaE$19kam|>9l@@y9|!05qcq@15s{nbkmr;_bboxvH((prugc@w91*lZ3hj!J zR~oCb{E<{_XH(Q6NIPbWAvFBxyFMj4SYHc+uOX z-O>(yxP*EsKfRft=r-20U~OqLCe3S9;p!A6T|CJ8kNsq7YQ^&9e2fKa(kJMQq<85w z+Q??!Y>j7nt8nAHX?b?`dVO`tI;=V^=Ch>TNNQjxM>VUw4HbkisH?D75mRaQBdNb< zHEV>XGEpiL?q(wwNe;1AuVWLPe0BNTuJM}J5TW?2Anj_E5Y)=zU4MY3P8h4bp;lkj z@@z)7ZsF19g6XYL3PXDNW5sDfNoYC*d%)g(c+A6TRO<9Ip(@6xbgU@EiXo15^q`4u ziepvL>~N#c^q{)J6+4M!Oj#-a75)=DtDn5n8}R3YAQi)^%{+PfsG)W~@fUMS+FKN& zjlv;H{XazvE9d*^v$HB!Mnkx~<5;%?Qe%N$2I4$wJBT}IjmDfFH)G!`T3X4iss`D$ zksZYm?1Y3MKiO3+-V(vAuX~O~t7EaKLuRf}{bSX_gB0Z`VLRpi*;~o%$71N+U68o- zkG=>rNXB@`pkG?GJ8b1QY{uy?Su7gfi4Eya{lzh33P&?qH$bo>O4A(@hh}L!hl-pr zG8Q1A6*p}!yYZhS_For~+Qza{8v}eLnNJSoq#ug6MT@5A-Nuj$EN(~TlzrWHUN^-k z-!X5c^~6LrJ5!YP9WQ({>1uH-tk?}QK78goNCO|uH}5Gj+uY)-cE-j8LieSYdPlX4 zB+Z+(N*QA8bQ~R4Qa`KTF4K5?m+;aQF zS%V|b46oyjdNQeRo^1y`X?`+3C&M6zXFH|vqJqv*g2J_f?83y9xk>CdR}YtLagbu$ zfU?l)0)7TdQjiPkYW_YMLIgyy;PQ~JyEF(NK|8M!ST6R&V(Z_YcYd#VeB59r?Ypw?`Ae{MX-6j!XuHJKI8z^1p)VdDM$h9NzSEn;mh!Ul<8%`s2Lfg zbLF%sAg>)%&w>f`lPQNJ%d}GmL_5EL@7!ec%LCJ9_rZwQ`BNiaGR95dk?Rz?ug)Ne zL7R;pWqX>Ga&TkFDQ_Rz84DiXj?TzFCHBiFErH-6b8uov4Ga;<$rPWOp(fc~S3WsU z`A_rmES&vp@CH zAh%Cu^yc)P@cR_x&r<={KMxDD%7AfNuziBE7<>`Q)P&i)gL$gwJiz>|+a4C;(}GN< z1m{{b0_4>I_?)Q1&;MUlp$gFrPL9f0pByEyHcZDK93>Gfqq8b|aF zSS$1G&*y-Q;(<7k14X9}i4|mk8wR&B_y|@2QrLHtE}k~U$3VWarM3#MYRFHvPxKPM z;UV+t72nt;B;*4qQ0`AWRqkiu{{vJjKV0Ae zG9m=uxm;I7b!yWFCoC1y@q|1-XnhLCjP-6^5;rt?fpb%o>I!cs!;%m7Sm;XP57VFvDd(N>{*JkP6=S;M9%#`7JX$r zPVfPqU3QB9f|@|SkL{wsu?RV^ee$y-DyMwWc#Fu%!R~@kAgaRIBwc)3WHc_inpSW$ zCa{A}1ZacrJpZlQ`(Ji@3G}L@{CqKY<H$N8u>{Jc z`Cm$*FYQ-+I`|16?K*S8)&m$T8!q(d#b3YY#`=BHf8{5ta!Hc%^f3$)31kgNT;$G9 zpqHrx3^8$sS)!eODi)mkiB&Q-y|??7c)FHRMLN%`eJ?eS8+SWNa3bx->BRMfan#4V z=`;pgM%4qRvc2B9L%w!01w(bPjtxaPkbBgA29!5l_G&M;NjS zwL4~2xWAzAcFLcmL8E=I8WcL6qeEj)sW}l8^acq|qK36Gj-*PcN$t>N?Rwwti*r-- zeT;{aa~17TlTPm3-1uXj|A0tWBVHs)aCM@~hljylj^EJ36cI0E5pVS$ zP5So*QW=1qJ^G_H01_f(wECjguD`>#-J7pU-{Ol5oK^yQ$4X?cN3e1WSYma=A`1VR z|Mu5@V9cz^;o#ZVRePXBNR8Vwc_H!xj1j;8t zOu{qvKk+vqGydQNq`*Ove+=A+-kktIYfb2_PEnx9?Gc44r!YF}Bv4=3ON;&IQ)1+< zSAmr_g6V!S;6|5-Eb0C&m;XRD^4UoMa_8&Jg_i+A0PNtv|8rk||Q_9iK#^Xrojk2XvLwL(d?CKTtSRspjNgk9)P|oj=V%pZH8KSd=plDX z0f-{C$J@&>zB!%;2~R7{S3Xg@X?A1|xHZlR+p}_lV3r_>KgyHyHi#OF+ z)jZn2iy;q_F-g^J=y#GsMFFoerpeKNDy$&!i>^tie>j=a!sI==yBW61c77{t}u z4x9%FG%Bc4lFd)a;?_lp?FuYoT)9ET(pQm}&_82A<1iA0CC|N9@yFajAQ4&5bgWe!fy#1%VfBeoeeOj1_Lg!hx?R{(B?VA+p zC>a(=R6lHl6$RMd^mubf$6EG#CLJ3pvtloCa3bs)4bm^uPp4C~(wC@SW6K<`)SBG# z+8QlcPaZAHl5ntr7)e|Dx_&DfankF)|9gxx{*xf^a^1P<*pq`}61g>|gbrujYGWzC zxz?t}H7F4FqBN=;jJT_w9d(y1Ef|Pd>O+_%5gXyTjo~RSi5b7!P|YV28u|K)t-F2gqMbZ|@9J5v)J+RtFR6Du;r?B`#0sn5A*YWmVUYHHZ3-LmO_535#9{ z7v4_e-GcwFRBK-d0RVrKC6m4jAEhel7rDA&d{{p|>sM*;n~`hgb-Ki773 ze@o(4xYtS^BaEG<&wE|l3xFr+S4u#4is1+DAgQmpL#`OaImIgM;WnKW=$GAQButBz z-tWKjw1+1O%)yQcrc9Jt%?vWwi*B`-*!RA}^&?!j-NZ7q(3FkLVgjSv`xjK_g!_L} z08n+g1eh>yM8@lTc)%xDx=+K%o9KBWi$7%+157p+AmW}J=;m;C^mo0fT3jd4v9VpO z_~B|h^6oNpO>m6E(IPd;o*B8okIS1p6PL^rgBErY}y;GBfl2QF;lD@xT zg3Q51iG5pa9(Uq4*Vxe;)L5JUvNCp?v7#Ngm$;f#TjB8#T2xRHqaO zj6-DIq2#!~(qU2?<14hZ9XH*iK^SKY?1>hNDm;>2Q2i;wFF{}FxH!K%ES^#~zdAv?z+a>H z`=r!0=-NE1CYXFH)b&T~>_Cq&jo0$3l_O=1<0B`FU7Q;Ck8ibKF5?n4EU`7xWNO}z z+}CJ%(YHnA&`20k;;qcqw@EGF@Q9A0AvkyDiVasbO}_2ExF-Fz%u0Aj z9M%eY!H}6&WAKIEecRm|Tt^e5*}I2-YD~t4gxo!facP*HTJlhrk&XQNLxU8|fNUf( zY&_;SJeYZSH!jkaL>uVptDP^!*@oH;)>4n-Hmc&%8?;x1#i|g?)NjIsptLzF8x7ii zf!-1}`6--j9lwDI_(=dJM!)|1*vsEsXLgt|uT~37?v0N=32|+V7;2E@@^!5>%wRr$ z$I^z*-h1SYK)SW75evhjoK<7;@aBP>}8~VxMSaViTHro zy`?g@#o1GK#Ol*(_0l(`Xk7jS?D(Kk1*Y?WcuaT4QWn`rG*xJ-DU7&~lBoI4Z1gqu z*)xBX>D&l?|7e%EehV(n$!Psh4eMV$miPIat8Kn@b)G@(@!(@w7VyIq|1+ZMPy~XO z-bY}ONF>UmiWwY1toxNG0)|FB4xrSY(`2d{fx4PfJ)a&d<|<^(_b&PF9t%-SWwwa; z$|h$8d?Krq=9CXOwnC2$|75H#Gu33+?CMnwe{W-TIm&&c`#$2MFMXv^<>G3pQ|t}e z98r?SrDH)5OsVXzz4mv9mlutM;p~(utab$6h*!@Zd82!sjvYJv=_;3M-a9O3zOR*k zZtxr6J_nhTtM4VN@Rs2o;|k{+4^~!|pfle#8knDITw>D5+cGE2?X9!wsj~lCEM3ox zSwdehT_2C@mnwYW;%S5yXPQS34$bL3%2d#47?0}N$>AU-T**xYkEk*kF;;l>mO)pI z4Bm54WW3qwkt*ct)-NqWo$u2!X?fqqm|Kpsp$^lRzCH@?JbD95k@%HD9p3Mr zll4pdgGs)FnT^2bYg5c#`Q^^St}Am(`x<`qAyZYoWLtXW=92@rR}FROB+`?do%f96 zL``h%1$O5?HH5qYZ4l;E1sf4FWBw001{&fmmD_WLRq*T;Jw|=gTKc`h{2%9+bhYKY zv$C?(4OEWasj%== zrTsJs&Kj=b@?EIMZM9bDt3L@Uk7!4gm*4TZKqzce^XfXGus$(jt&whRBJVQY{zO&o zxA3n5v&5i9;OXR|eB2LL8WdC>yrFy~)nDHm#BdbTz1emog*h^AejzRQ=e$x_P^6h= z&?1JfttRblYVhGA^d(#*tfKJ%tL&dMbvCH>qVc0<+d`q`^2|MMU6 zo#LzSuCl{II~~8c{d`?3r*qdKQ&%r%cSmSymbeCi->-QeJ3eRNSFP&Kq2`8&vw6fA z`u56U+_+sSw>Ik7L_cPKvF6KJ9om8G z+J!V#s>%=E1%6BI`?~i*By?r7{ymXu6UI1f_RkVWMPlND4p=Va?u!Nr(jwy2J+m20 z&B<N3jr>UbE-%3?Gs>y* z!e6^DpUk+37(`^=%lS4dlBjRFs2&v^%>aJSf~MSg<$IOq!CHqjF8C0#wIOS6{)4%A zUw?9H3fnG@86W@lW}wnTB!EdWJz3m!&Ddt7tWZid2G06FB?+#PN*5l}&Ygu2m_O5( zTeZk}z5<5UE_78tGTMTh?2ZqaUi=~*zCExLGg`PS4ptB+9eUTRCY{CHK|69 z_X`gO-FH;iC1DwI(?TAe*scOOw$0t3fP*wWW33C``{;<=J>r zP=s26+^b*P7EDiF$>(4SFXpB_-%iV4puu}s;XmLu{ICu8`3DE>h~)X)G$vR01Cl<* zH3i>J=u2z0__s^5*X2u6?S%~uDPM2u^Nd!A-m9dJOV(H#hBrTS2;O_(F|j@B=UV?_ z=K-Oi0RtM!+L>d^G#y$ysu4R+`Q#Ni0e8=&>FdN9jD#P%ZU zPlsGVx20L4mv)A)b_K~K4(0Ev-!9zUM?Q63@_$;^O=izW+R*A?S>o(Uta59~0e5Sn z*>HMFd@4Ap*>5kXS~|y@y1MB?%Y^xA zwoQo-=UQIqORufA`|t0( zE3m_X&%z~GNL?N&Y6$aOrQ`j%3f=99V9M#c`F*>pJGNvlL2yQu*XD~|(QbtOMy&`v zVXqil`caJzVeGEhrL0IQj$D0Lor_afla5y!&K#E2(%flGnp(dX!P$KH$7?3&;zP}S zj0XbSmvcS6TCYP@`~Knf6d+lReZQ!!wx$2peGSQoG%v=g27daBQ-zhjz(8N`qI^t) zJ{j$?a)iEKAHm#ww)71h;kP9=o{|D3XVf(hnGD!76ccB%_{2GwiDIvs$$Gvu5D+|9 zrBd6W2j^s5FUBcAxoZ|hp)QZQ6qv!1B;4g}`1WUYHJ;y4`IY~dQXN{=wyw$c3znVi zVVI9lNXy(#lbY8r=;r9YA*CEQZaMbpOa#w18*}!>@AlpYoI&LeUsj>1PpPEqkHK#gi7rt?r`-wtmLiBuG1|qHZ7xk z8gmpuB8Q;#r8Dh*G4tUO)r1{~pT&$d_SpVjbJeeUnP48U`~;owNNmi3tMuG!%93S;bA1_(#``C|J~IxbJPF1v>bruN%)J?yjz+ zeT&p){oMbn$}=p=z79V2#dHTt!0j!S@RF{jc#hK>TWtO=-XPd>Y*9WcAN##{A!%ir zccP&`aePKccy#={iKdB9Gg>a+`w)z&`DxJRyzg$78hOCN!*luGy?d+6A3cN4T=?*x ze==`afWh6f(ePL8`Z*m1_3H>{{&T()+8PH*nPq#u-A_*toXVrtXCm@3G_ig->G}Yo zlU~&*G0F&;|!pFf6ZvCn9op$0X<>hxKXe?4Wu<&wbZq8}x15f7l-;H=sv4 zWx{dC!V;xq%n6wZy2tk8L7Tjdz8qy3{h2lb&YLt;W7{y^htIC6z(lHw7S(8Tc)DgC z01QRB-q#hlDMf{OTuf#Nd9~EI8`9@_5OxVsd6QJ_8`JK>!kS|s!vnUxRIiPcmx%nE z}|$xSl||M~n1c$z0_hZ;7Ah?kacW{&(%t*^5(`%`>cCCah97kfH?#UN+I^dy{A!k4zqnRGOVNj0lYv z{P@N~(&4}-sN}1v@G)lnk1e7KxmfQ~sWa7wGl3S_O5Z!%OBaQ(P?`w?XlgxlCUm1) z!1j-K%1l=|VuKQ8@8uFdJC;vU?Q2=`P<5Ng4*dS}dOA6#9o3k}=(Rh)Bh%YoSM`fr z0LE>zK`OMiE(nEwE$K1U=Qfw4EvD|2e?hbKsW|L>qRSG3L1OMbl5u^l=g9*l`aJ4} zI~xVAF7o788fn)Kd*sZ1K~0rzl)nhdW&3y(<$qlA?2mU!>l}>lZL|49remPt`Treg zlt=BdGN%}CjfHr;(+l8$4En}igtOfz=d}H(kJm+%+hiXMSuAc)%}bpqmCp_mOG$FVx?jD7RfoF43qL}EZoxT1}GvozlQW^0xn#VJ(U zr>-L({Y#p}kx0m}(65hihYJIy*5ed_i@1K7y6X+8w{I>i<#kY_MGVIGdf79#8gSv4 zt^DU_2)u;{Qe%6Qv%@>ol#!4JUs68+E77e%=b(jXs3R%s@ zQJnO9v_Ju(?315-U-Q)oXVuda6zRb_U6%W~-&GRn{Z>?hGXOqHDAMxL9czUxkQ;#E!!zl(B7+0h_dC6ppNvMLh|NB--3#~n_d z3RP&omhXJRF3i!Hzre2#F#(fSG9r=I-?`}gj<4RX#$9?Ir95SzLlm?VL$GK-BSt#| zck%O*A#8JUx&LxQt;IiAV~0r!z+923)FXpwsrSM@0O_Bsy_M#DL`SvkK9G9uAa!w_ z^Pd*w+XKwfn5WHrT4_NS2_`eC8|`v0n!T*ewr?~W#6n%at>|jnKMw*uSm-(U-Fr?a zWoLEkQayY{|6GvWM{A(7-5JG(2_?wv;;IpRgnkMeAW{LC|NZkS2-p<=YHe2b$xwVc zJuS?_tXj)B)HcIrXAkrfL{{hRJOz>GIEE8sWb!>Bse#Uc(ktKLD01S-V`I0>(DE0r zSwTMBYsorJd(_Z)eC>9r2`DQj1RIjJ$;FSH}7p^62fX`Z|L7)A!Z&g{&efdn- zY9{QlJjAm))%TB84XXm@M8(2(r(G#t93Z`x!-!u{8=^~T5cZk*#jeqd?DLtUY&0R zuHwNBEh42My_Qt9+O;^#n&2tXr36khi~9~&)@G65kSzuae;%s2>=6r8DTR}uj3`|Xp zsKtIa-ZwPV(-%h*m8h{INQ8?UjKt84`a*bpGpo=e>LMg0M73>mFjaZn=gXJjLRFfr zGgtAXBCem}?(RH;X}lq8YsP0uV%Z(TnZjtOs2TVB;Ytb$862dfr1=LB2!zo90-MJ% z=FdofdUJQN7?qHaH?h!QZ~Bsu2gIejeg$7>7@L|JHeRGoLPaI18h1KgVI=+%m!-Gh zuFxF6RsP?rPC8r-uK=>vx|xquqxo_)h@nUQdc8E}1yB#jM7OSt6lfuNJd}05kBg>% zA_HTn+=nz7{)@jh0!Ocb^;@;MVgX_}GGqFAop6cYS{5G0Ttm-qG!-7n@ zoHagUx81QORkEA!-1A%SQlk;%!s3PegD9`5h9CQ6b!&**6S+{+p^lB8y(ga#6&$#9dis-HF04wwYY|3N zp-nOMyht&H&?z7qwUZ`j9v=Pk!@fzJp_q zZ^pRC8$aHK?rdAyUou5N-;neK4p@tIWH5wuvDTHC4^AAmvA%^RadYLI{ZVi|h`4&O z=*7P=kZfxIGDtv9o}o!tCjnBQjq2|19{KgFW{#P~0RuVt3p(vu-;u3E>%@S7fZ!}- zWP0lXBhI!6{(I0{(?J!SmcdxXY%zoVnVKBgLX11Qku7jBH!f#tQc`eWAiU#gXA~Hp z)Q*?*+g@&zV^b0;)URYN#im-KtBrG~sglj@;=pgt-8R=@e+CN<8Q&%sVK@4JG5r$+ z7F%FNk}sd;_w*-}b!|6jnQ;E3ZD+cPb#2C%D`CPI4loj35vH7YAw+TT6jz`qEscHm z(}7ELD>D1a(ut!q0-$9N7 z`mSAWlLT})=RDw~e*LJ3=zc@jBtqNb8OlJ|3@*p)Y*l_X<=08z-b)31w&b%wGgdF`4e?r$GB{>{_z+I zs|vvi)dkmGI-biVAGr%|rvsfir$vvP{X&vZX$o*K4wmYj1GLt1meIHuHE)yVRoBm4 z)1frB*+y+pLh>sjk6%%vr`4?;M)Tex=lug{rXfEzeNgypYG3ch{JCrfkIQznVZRNy zU8^@oa~n4X%;Uv)KHB6v0u)&{fdYKy68tZ>C+EGf9hLkb^-bkvSKKHlUMVXMPB^cA zK~LG^MYrgu<9$p^-1`pG)*`f_H{A1ej?XHjUSFkaU~0}2*PO#Sk)7xkJ-ts-ui{$` zM_dg+AyV`FaUKkKZ zuFE_6*=okr@P?dmkr}qSoNkoTwpE9nH4j+VD}x^Ps)iUnt{oCKUlS1M?(AF5O9mhl z*z8R)<=xx^Nd}Y6l0e>U#nV5tz)>>5hixD5s>{KUDtN%8AEbU6eLVPxX=$)K-vY>-k2<4<1)7QCoKQwiDDn(?cW^6n{7&Gj~|ZQ1({d^HeEji`U+`etg*UbO&j(bY(-L``%8?FjjXun^g zMkprcRGQnzkF+9~wW>Q>pojGYeMY;r-k%bOK=kwVn_jfg%q#g82mUmxon6E0AK15< z?!`AzqHUTGbZM!L?FOk2Yy!4kNpAtX(WfWzQ#t4(5#u|7=L{d+zLcC11_gYIAmrT( zzSQ?P!zyjNzeYSwSc_WiiKuHR-7RfG*7v$|+;)Q)Yz=2dQhw0xj+gidq!ty^Kc(jz z+t@5;V%;R0+q$34sa1r}le)>2SmB~mfyly!O77aCX^9IwQD-qlgV5{Uk1cE zrAwloNAw7aOs6NXZqPm)hB8_ewKlUIwSwXTmHX`3VV73~O~7@{Y^I7YT5mf&VUJZw z+>gD~6Ev-PE)5VT_6O{72dYo+7>@cLrfld~BuQ+g?eq(p-D6L}*~2vw7Bfxq_5@7uajnpR^u8)V}ieFrQ$?}J0oNu6V-Fi(3o(p;lNmz$xXq2J`XSUV7UE$vVhZ-DrRwcxgy z>SS{<@08#+pTxWbdYT4Bm?%;I0+oj}uMQ)`qoQY@N>~2o}ju$0j=* zUegTvcQpW5(_#RfY(_nj8{X#V!vVsh+q{@~T>0O+_`g^PKQ%WWfz1G_h;doN!tY%Z`?u6zNmhbtuM- z`L5zf-iix$qpd|%Z_`lpwKwLFHsEblZ7uN9S(n;3EfHkS%;dh9g zC0jG$b0Xo_=Z4t`5%&yOD?V05FSoXWP!4E9;D3^gXYD%Lg3ELPOx{oNcvnj#2BXpF~~IRE?rp#ARd1`DY_gYfCszYGXZt$p3)qW3_U$TH!HYC(sO zAr{*7>?C9{&tMsfpU_x|2~}ne16??OWh8uE+__u#Jllfi=A1ObGy2P-Qvyyi&FM9E z)a_S>(ekTw_iv_n<0;&tyT|)RC;4KME{_GM7V@TboB=ee-t9U zWlL>CCsn5y{>!Cjy`!)T3kwJLjwW7zrYf?bk9lLmGCH>A$1#B-CP8fsgD|UhuzYWx z$u4SouCR&?(-(IdPySDxjH;ia^z%CkM0nj_idDnY2~|`W>oFCq(@C)6w)cz%24)yu z;z~K-h)YE&`ozkV>{OX(ylLj0$F1J!#ni|vMPLy!m<#>&m7PySP_cV)o(d;S@F7ii zCz!|2)+sW<%r*$uDMurCN-PGUjG?!EVD)yGRsv4^p8T$v3~tCnnU5+xTas~I_*O>f zP3tWQp1iI)w&=~2X@N#V=T)GsW?QyV6Qobm;IKjG-sIYi(J0Xm%wyR=IY8(^xx3{N5wx~Y@f~yAoSE5tF)C2~@E)1BN8MUYj@d69fuabm5{!_JB}ug#Ty zQ$jvD)N0`^K!OeRTbK+{GhAFYS{E>{!R2w7b%aMK<+Rw4`VfM)_bbe`+`en0 z2%_~Sa_CkBhYUSosxb-)Qn*%-K6{d^If$Y0$a322vi`K+@i7)#6R(S)il|hQG2+%xd@o^E#LGw*h|q!|HDycY@=o{E*za} zeOdpM{tM!fF0ur$6p3uiEqe4|fYCAGzB^h7Cg$OwdX979w2yGHLw}&rOz7l=8RWTt zaMuXZ^vVxREH}Nnzw{_$ zc&{JhOIV^aYEsxFS6e0w6Q%>gsZ77C#|#Kh7&_co-S^8oNY=wy0@45o@xomENEZ(C#qh+h~2#2j=MtVyi!Tm^XP@VS92o>7R zyy5k5lde6@E@I%m)*CfdZXg_HXGRIB7wGNn{Y;82D^2rYENSu?pizU*_h*~p-f;g( zpqkA+LYwA0%RNYa{%h6lS6oUx%~e3^#xVJO$y-{9DN{b^9mAIf_$z>0gY;T8I4{q1 zAwCVQg*u9WoZhz%j#)bA)kO`~Bfzg#+CO+~A{lGWteAh!J7{>$$LBw@paBGJvQ|TJ z`m{^hnx9rYyet|35WH=vk$aV(ZYJa!k0x%2c)Vym8kuOI60uI}N{wFv>37qZ-gW*( zQqlW^Rj}+?W6jUiuOB)hMbh4QMo&)^Y%lGyYC50hh4xJrPQL3;RvdKPduIv6UXca0 z4FbAP=rro3#SXF8w4w4zEC>L!vGBXn)-Gs19ptu>&!0l|JBA~4eqz5l=y;^Usa8T> ztkN~f%`o&yNT=t$YHK{{C}z5?o5>@3x!x(Faju*#WN1?s%QlL*X<$~b{ZmZv7cwm7 z#TsFu&<`1R^Emn`pILw1Ec3|lPZt=knZ?!P5$JrQ75Wx9Y_O0E!wJRH!kS|x0+Svi+epW-z%KX#iq5JuuzIq*&jy&pVD4D@5XiBk%2#oa={TU zqtUkhl!hJjnyXjd+6Upov=+~mWfdaY>I9_zmklKp!>O4lNNoV|xUFe5V>B^RJ3~|{ zSFR%3CHO2jbe1;5SjP?G>^ahA(KjM=zn|%+C13S9ONq$tm_I`PLP76%1ss`1@+IQM z@>5axV+Q&1ry-N8{m#HGAlnsrKoKjX+>yyG`2&~I#JxS`_bJ!7_} z3ndTR$^PmUHL9iDc|B0DJk5q?pmVatdj$K;ucgaoI&GA;0NMB*P=%@|T#^lS zk+9{QbfF-n!HdsI^!VGsAd$XN5(W-u;l21{DmxDp@ z*5X;K4=H#h_r#4&Qy9>@+uL%dL%nLt*@=Tl09){+xu1NO$}=9C0R!rhv@|S?j0ML@ z-9&ybPd~nr7{er*U;DE-O-_fU3SLJ`9jHa9Zruyb#XV>Y9~uze6=_gJI5=379)B|_ zC|XM%)F{B`_nfn;uA;&B^|G|A@QxwSy8kI*$6;X;)h`zZS8%n$xV z49xDj71^(34 z#>}s9tD&!8Kr?XZrst=fUk(}l?i%;8%ET-)X}2ADy+ox|11HdpBKMtpIzCx*zq?cF z5d;v}7@Fnr07YgnA|mUQ?HrPR35vpZF8k9OsX*+e*j;hs{PbF1+n}8UAkmf0K%ec# z(1R?^`r)i3oiZ~Piie}O`0c0Y3y%>xZ-q`d&15tWQk)McsR6E2`eemq!@S~e$;)>U zdB;3~3D53GnP1ikW$3|Dvwqrlwj2}6^E_Lax*6viw}jIG!L_Ut4A-iK^V#S)&5TgV zmp{U_pu+c(ut}oFS_kDjEHviJ)V%r;1CQ1YoTc={^(%;ytuAEvkh!E2bGa<$I zr-k%<6}f@5)R+N2{`*vY+Zu{DKs`zdyC3z&3Yx znG9&?$B!=WXK=$UZca@EW%_LkRxsipjO9NMe*a!U#W7LCNRQTzK_N%0TQ-NkHC~i3 za5k&$Q`Iz#>T9c6R903tkdu>}3#QDc9VxEbWuHucYl{4SAjzc4#RT}!3S0gUjD-z67MU`koB>ZZGI-J3G{Z`Nz6AB=k zphmk&uSx^x_4oDPfG>eGuxQfBxL4&3gb58zTmhmCnD8#wmFtF+`NeP1ik|{$RaW-A zt6t~~zr~BtkIM2rE(vHk?qWBP5;&Y?(?=LgMt05iFwU$59UrqSml^3fJGn4I&%Q$k zvUM?_CcFsSgo#xwpxJQeZ2cx3-z&U|i&zSDW*(cNE)^XvM(Lmluf5am6-!UhFrR$T zI`~Q1{Gm@~s@?HPwFoA5Lg19VCq^!~>ZYb&J!;g8;4Lw)cEAX)U7t&+7r@w%zv@+r zS`!ru`Zj6Q$-l7U?;Yx9kp9upNHFWP+fa1p3OAI)c@P5+<@%lRB!1kF(RfCB`)NnW zXMm&1bnAD=(xrE+Fpof)@yA&iNXAh4)?$->rpdu)Dm-x^$by>3kb( zyzbI^T92c5LniXv@Z2&vPjA*~217R)XK7hlc`Rj}y!#lI_l0AxPr|}Cjb5Jjx+z}( zazrRPG8n#XKM+UD4=dBi;pv*U5$7}ZFgbO0ehnScVPD_l?7X4n7;7&oxSeePht)ZC zx2xcM;QAQ~sV9l0E_%Cl4{{VQDFnmRx47CL!gm}U=;i2=e#0_aQ9fpC`o(Hiet&kN zMux7-w;bfG^Y$kkijE%GFuDJ_#U=5~Ab6ma=d3-RJzIJ)hM!cJC{!>rOk~V%azxS8 zZ-@|4c!=$QzgHDuwK7s&-vcXx?>c|r%)CYm6BAP)ugXKYBaM*|3(GGtF%i*Y0;uVu z^#awsng&Acm4X&l36>TSCG|QMSOcAUXJ_ZBYMAY@0t{pVE*0IE`Lta6u%>RNKkSzH zA8s*Dsy~;2B~{3%TiAkIc;hIT*8DM${+ZVa0eKxs?2bBt8{z7QX|B0r17CE_wp6TV zK6wajRu{@yr`ZX)OIexJQDbp7*kn|Zy9_2-V zy6Xr!fM7{UotPtL;0c|PsS!*(u&kvoj zV9E!_0HTAW$Y4N#SOD)5M2I6er3LajokTU6UddsHz`p95Af?dv85}?Ycz04VCFv;J zb>3wvv^6lJ^jJ%q6BAz(g`DQVlvFEK)RD^-?j#+?WWK;{6ckv-c0Eg;~v;^XO&JhxbNL)b>ug0sU1DJE9!fj%t@vQy`3l z!^3R9k7{hg+&9$Bhar*$DH)yB--C z;Q;E-ehtBAK_a5F)L;iGCD9<*TGcfnHLE8&j^nCR%`Q~~T(1N{2|*|z#wXmL_R1Sx z_;h?W>Iu&TYPE58^(TEa`&x5*(^YUSZWp!F_dyhC?Ew9yu-x+NmjmpJ_%>UULot6A zLRNFkcv?*uuZQS7+2WA7HBOfk(lpl{D))!G3%1=km(5gj`pnGCPhMpvkJS-%=X8j3 z4L{=aAGKW;s^1ypr&$cBX!D(w@)JHyg222oDQG)#gY)g~dybgmo`-{Q(Ln18bf~ol z1G@UlL6s<|k_gYejcNw^h<+Ot8eJ!%?XH_*FU=DxB7JlOdZV=!?Gi;^POWv(&7|_~sjY#C)X+KY6}04yOkdk&QlW;aALaEy0vH(3beB~lOw72H zw^YK{=j}PG;`Io*!t)9sCwOJDxK_=WFm}(v%lwF{yH+=Kcz*}F5ZnZmwx<`Wp-Q#b z!x!n)?O|mJLDOi0T(oYj%}1Q3wp^_`x+mftI=L?|I*cY`>taDR)}c9vEmhJAXJ>@C zA3cgb#aL@0Iok=6k1U_}0p-WQ*KgmeINu~&w|1OO>RZ>PhGJl=t?UC$2|2K4lAw3G zDQ7_0mXDdaQiVlUQc@6Ls&WuSyOSHW>#VS#@y6k*S2DY2t`eBd#o`*0D&@tZv=ToQ zx1ya#;>~6id~F-9-s~0^_ukmW5EBy%sP^~v=C^mGT`6999kSbpi!Wn$)~0NN+<-y4`|t+DaV#o#cnb)|SR)gQCO>1)h`H8jFDYyPMSs zZEL^FlSr4~fogHMLZQCn9>OYmJJrhBkBjW#Hjg0F zOS(g`m6dxIH8|YCNtxtMrM$zNS&R(5n5un-DD>*E?>X})EDxb&60__4sQymz5_A=@mF!tIh}Jk9en_GJiCgzWZ5<3 zb+w$(b{nalud29UND&>Gsmw+O4wa4`nDj*M0OYhk(qHT2cGK(a0df=t!fh~Z2ygH& z>?%st+vHrHCCfe(OfjPD{HpUbZ_z){f7_nv0o-*4n4j^A~>0q>{QWF^X%;P2 z{fR?v!yH2^`^zH(hl9E415_}dHib&D)SiPwE5N6QpIekV9e3lbPU?Hr%TdaqUK>q6SX`KXFC&+89>pldd&uvqvHN?!nm8eXJT`Rjvn2aaS&2KOoc z_LwpZIP}4z59z`a-+Q(vXITu%%Nori{eelv<()noC5E)qB~^u+P2QXm;7ja+0zJ*8 zqNT1W<0FP_%Lvd=Ri{E@;?E^8-JAJq7gTb7J?T&HvH@oGBVVr@_*O)RR(SdDo zhJEKH5-08|(;#EDBvcAqlR+`jj@A!kHgM18SKB}C-YbfUO>g?4E#fNaK;=ftw;C<* zE6?&?YR^dwgfjt%#nl#8hxAgvMVBDDRb)niU*;?lvELZY^u7d;xH#E$Y)yZn;X3$B z!?aNf#QD{&Av=@7ZQMnxfY$r3X=vEEz5!NEwJ+Ld_M?GHp&5^0pjp{5V;0HFRa7 zuSQ)WNGcAKiqOLfEE1oQbKNpekKa#0gXYYS6)5j($y9$BeL*3YlFwN33Xkm_P!AhV zvXTYzR*s)(s=@2u)^k74;oH;%jt7c>v=jVEL;L4$&i6y+o1B`0pSInd<3S^ctgAdc zJ^4tP=e{xnDw1N2iYy(Bs}t;d0jq6d=i0{EjbbhQ%F4!bHYKvLa0X za>cI><-qwjAQ=wqvK|b5cI9~T3*8jNmH>pCArMfX zdb5SE``O7Vq~|-VrZwqx^><|iD$&3Mk*r4@sUhGuR`~Bb$-Ug0sGcYXm=yM&&$++y zoZZj69Vy|3k|o`2Fl~dTWz!%N`B(RV@YkjPOWY-`;<#z_FDVd?HZIy)_l`(b^FtFO z<^W7^EF=9sHV{%*`0sxAO_bS}2BUi&^e|$DF|d;xhp!ydoztPe0_Z%#9raM6y33-# zD=J}TEHAX?ZX=!MNw>Q1>OqQ&8t7l{_wAUBJ~qw>zZQpne60l(S5K%yC)dhcl`){3 zUSZXtmBjN)PxyK4HeazyPBmI1t{!9+SI4yi1DTH2`atiSk)ZUJW>#jlny7~T_Y z+OPfXB^uL$T*h_(s1H+Vy3+?DaU(=qmRxuoWJ)*Lh1gcnF^0n2{Kb|ULVX?`y)h>o zZF_#eW!V{!W22w*bOT~tzY3!swZsj8Bnoj_aOqj!o&+X*jsplZ9G&$lYXN}~x6Wea z3P7X~6V6WLWFo9GdmZoSwRyHk3p2v9#CKb$$+Tl{|R7SxMPP0caWTDc->rkHsXA{Nzqz6Y5=2+Cb z@x~{`Q~P#XDy z*x5DaL_R|REh}6^G^dgAwmiU_@-kq+!Ic)zY&>(RcWSkO#GAA+EZ~PC-1Q?(O{{tz zyJ2tKe_Xs4;p?p%2#Zgyh>QO%K+1C^GGvD8pJu5t2PM%0=Ej`h>kHfTotz9I+u3fG z+H^2Qut=>WMK3%srayt9=Q)m!Lgqe^*R8>+qxcI>piwGkg7N%1axK+zw$@LCom+XL zRJRD$WcMekpRJ={WMW{cZWFb&m1*$aG$8e&wzNCn(_E)GkSN&uA?YURYW|^SMIs&D zlKx90{7n3MZn6~wN8f!}0w>228itq0(_@nMfAHC(CVgMsVmO8CW~v7W}Ff{{))n1v>*v z0Vxiw1&2#RB!hzE*J5!KQ3;g3(F#H@2Gx@Mik*f&_?+y?eIF~h3S19yPV46Y3Zk$m zsgk1B)_iL|qF&bpt}9pzRE19{Fn(=~lM}s#<`!@nvXVe*Owf>53iz5W=m&RWi|Qp7 zo8+CT9<0!Ex(~IXtj>uln0GPiUT1hz<8F^C6Jkg$00`F%? z2YvwVX$PA*I8+DcW2JAu5%w3zvQMoTkMvDIziY8xmdxdl5!ac*@aC2dq??3%?c(Ebs28&7rL zo36BWg5<`GW;;*hPWq>$c1|I0^~adxyN6i5IR37DicZD=u`xM~lCafGhKGRPtJfv=#I^2`m9X4+hD0)) z#()X;D#of>m{BIv2LB~nuQ4L^jgi(=3`Z(y^-5LNQmDmlNmsd+5CxP5=x1_em85SkEQBP-~B8?C+Umge7}23iJxtd z*n5B?vv~%11wdQQBX*_*N??8INsKKDTo9hNj=X&CGJjCtjn@i#PF)R^8t+EVGx7ETz9Lt){SCE4KAL?iZ2@*O$8~A=>m30Rgo#i;-q4 z$_Q!B*Eb656rSV5!RHiV0WZ{rU}_`;5LQnWKbn=LaU?ucYgul`tkkBJmvv7!c;u@m zx{sRVp6P)ir0JmD^6LDr^H67wYj1py-4>*w@m?vX$+b1oK3sT=4{3$j)4B@=Y@~mv z!u}ej8RO&75=#p0vcT>X@122uK-x3JIxAAeb#z(6Jqtkjf0mwDW~Wpv03+-D8Gbc& zGkYoq;!I~*Cz(C6<*lUr>Xe{;N1RKWHiP^}-fjyc{jPp1FPyxmUW<4zSlB^>i(6<} zr=AM0>^3soV7{5r_A?Ij;-bP$&h8}aawp;=?E?**R>=!~o{5)SsV^QO%z$*zIG6qk zMeBB0KCXPNW{G4a(?9B*k17L!l1I6>MLs7g@Y@Jb0H&x>Bd6`ZcmuRA93-cQ%EIYH z4C~!GaA>ie3cN2x%O(o4N0sk3r-L|LUVTOPzDr6#w_(Zc&1vnLG1;eyIy#+b97_xP z6P*0@8E)vepEAbF=F8RH|oABG2>YVDicM+P*}jnV&Y-?p!$0Q*i2q=CdfkaN`Y zWN6dyw%*FWRpqB@0b!&SdxGU#;L6WRqD9hPbBcw!jM-XXlg~Oh@BXD8`g6UK@PTK_ zTJ<-)1E%MMk-;cuFxT;RdP|esP9m-%r?tPJ2@TP| z%oksYOf_||DR9)@pWFnaVHKCmO4>O5=bpLB0(;i7KqwaujO+~{(RiG&{meT+->P4M z209Tqi}}FS+qZ81mw&j`wHosOGtv6%x2zA4Zsk@C>5u`I{0eABeSFI5cn0v4)hK(v z#A~}OpZ=0NbKY6kl`6PSbFX*d692imOOHB#8*L~8pa~vh|J#d_<23{4QKy^r)X!0b@inE^JeDErJ_*v&&!V8Z2 z=fz_Gu^s|2WWWtQiDf4Qq}>zrUFT`Pe}`Q$|8u%66{yov$exS-m${D<6Mv~U z8LLN!j!psekS})La~^@khdqInDIO* z!C~>orbKRdnfrdSeMZa=;vAL=$OtDvTAyV8%UET7KWl>6#lw>KRal-g4ZpYYXS!jO z5#1(Zspeyh<7-$Dd1CwZaaWEQp1vYon1shPfzf)#l9oAqc_HRKB;kzPEN!Vmo866< z+*Y6QuB(2x4IP+CDgkg8(0@dY&)Nuq#Pr=}Eb&@}fgJP?*7qN%(vo+Z9MShe9}yBR z{8QRILxE+0R|7B4!g3!vbTL-?>=V(}AcTt71S9Er) z1MiI`d57J&o|nUDb?;;pfsF$KMs%0(1#Pvo%1HV@90}mcxF6s9vB29#XG~&mOW24rf0%1KiX&cQMf4*%MLnUxw4Z4_n-ATg7Na<*$tC#Qo_L z;3$TlAZUcF#w)H=ra+S%Mw?Y1hcc}6|E-Wf7%-zAs^CeG`QvsCe@4)__SX(xd#^Zm zV){%kcfLRwFcbCv*Gw|R5T;JhO}{yZflW7oZF?2_na;bpB4nkK4{^1W2+&jgKTD8r z3o{|a0d!G{4?ca|3F4>H2F+CPD;soKj0e{SIgAHf+_nD+V1SrRCZO1?W^2y~821Z_ zb}eG-&(b#LeVEWxh-xXbpY7yy1>jl#(-!{qFJrHPFjATg#M}S=g8#)#-dsSl)STll zOsKADPyByz6#v`qreF#XJr+XoPF=Tx_&5{{Z_jJJ6uND72 z0dI4R{GAY4)kAeRn7!Yx+(XUZkrq`woW#`V9}@Kxo4{Ppoh&JbdiKOIlzat-xgr|C;d&}MA{eAmO0$}$%7LJoD2(7U5L;z<>0lU**W1dP& z_&D+Je@t8&wpcXPFdMDATMQ&iE^zO2CH}ol^KqFn2I0+E81-rU@YkdR)QbE9*D>Xvx zCygU!ESXapFRwJOU8ntwN`3_R{0mjmA zuGlNe%#kt4`rz=Y-~Wx5EMsyT6@M#atDM*w?^g{uu~x`R?k`J$xje@A_pg*~t$1^~ zKd^#ev>43pnuPIec~N=%SDbyO>!JK3Ct3AqucuuCC8dPleW4IRv8-+2aLnW2{T`Ao zS|B2lkctNJOIH*uLHe9rTGe8HcG=h5@3^?(prHhgHSXcF5(s9uZo;#-^k!egVm+fu z2lBwURBH4^xUjVR56qSJw!nP?CozAGr=9O9z~(PKA#yN7D=k&FxS{H4oC2=sC{_Nq zy*qj^LSOi%T#pUtqM_>?7N{z4L$4O)F~&}+sb1Vp_% zm=xEvs1--|7r3iOxDf(oS*8LQtSu>wR#$$FOr^;dh3T-x+1(hU%(l$rz;~?9?3*}f z0Lye0YabO37%U-K+TB-Ooh&>v*anPq@%g{qicPS9U%q$@0)=68WMtB|3V-qMsN*Qx z-me^iOqV1GYu7J4g0# z8q%MDfAu9MF@D{KzxX$5J9|u`_Q?!y7a8y-A_^KU!Y3dKwzf<%2uPWnytBH(z^yWr zWEmr_otQ32GKz0AN4NWeR>RIhSnAq5(aXcJ*^yQ$jAC{-eK~>k03enr><>tNM1pC| zr5gYo&z?#A+f0x#@)I4Zy3hq$mjJ8)BB6Oau^0eVR2PEmlP)ib8LkO(s&h88%myS$ zU4Fx9o~3D#ODwn;Ptl-ZQKJUIv}n+E{;Dvk{8F})5j-VW9ZD=Raw;_E>@fS`YeSCiyvR`^#CQD^4c{HXL z;_AuQ57k1!z_G9tAS_4^-%$7m`z;+DPRRj#KaKi zJT%H~_@n9rdQN zk8P+WJ~o`Y(Uuo6d`Ykxw{Jf?eKE{LabY_ zvl$O4sU{vJ&)(*!<+opquw5xb)t^x&KMsrv2$6GpOnMltVVKZehvekBC7=axE`D%w8Buz$k75Lfu=#xsg$z zeN0iQSp4yXj6dGXf_pR-3;6DZ$2|S8))&L>v7}=4xtD%UD5C;?6(|G)7o4QXl7rnp z?M*!u^!BzgP>V@jTVFRATJTu+M;&2fWJDG8ehQC_;H{>NF<8giP&jj_J?Lm#p%}El zl{&Ej%goGN{?N`J2#+G(^vNX&Ov@WH#^V)4IPn3;ggbOGd4V^CTE z>>fX@Mj-!0ac6#1CmKG!j@4UINTXLHwI{JKf_~N9%`#y zt<|;?JKY$G%tKzdSt7w2=doL}33N`2iVCu4V&bsh2u1HHYx_hE21Di>n-dc?+#gn9 zUSnS@cm!P2k4;Gl3BkmdX#~Yqv*@@Ds0M}xnyE;Cd(5v#p2_}ct;55<$AF~NaPy!o zDkY^@jbNepHJ9T~;Q9Ux(-zdFy2GiUwu?PF_UXu_chw)K zVF0Q^&`ct5_I0n{{c@*xxzQ$lD1n=2b`uBdvlz!dPdDeSeLk_>PxX7ihe!ZKML@+} z4EJbMKC(-gnBGv*&@#;?LV2EZc&%Eq<)Z^Bk=hsINWG|nqXbL@?+tFRaP*--8yMpq z^n*Ep9WP1NrD?jC-yU!gUA-e$qvO7kL71Vs;aw=ZI_EV2>OHph`pt z3Zj&aFpN_-{1%MPZe)~~nG(a}uDkYf<9`8Vmk{gO^JgE_il z>ZjWh58-2q=~QfkZ~TsT?$yT2&ShG+uRVcC@-L{`2@5p*dfs+P7VDgG-*Y;{dwV|} zc5^1C;fC{mdY{O%jqY$THnOjXk<4b@(IFhbv>x0at8b)>bDG`u=1s>Dv-6v_j3=C& zoP9kCrDQPY|Iz{wJ~W^2P+3LTP}9f-7=&6TKO z2&>S-NlS#)U=PpM9r-jV5Jv{TRm!K-LWbSgc0vQ1$pcSMPxG{n4#8jxMMWkhW#y>& z_}pS)-=bgZhlgdC-)q`1fgTr~e)7C;`SmsFer_v=Y4U*E0(p;olnhQvSbfX}h(sk*Okj@KSknV;#`1$_k zna}ggZ|0BrYyRQoIo@;5-fORXueI0a8r9r&4^lhXvIgpBnGbwcZMYM6s~1FpT@Zzv zsc~`Np~g0~ zr%vlzT&34OudAx6xGaYlYaHf@V`HNjKBnWrfKd2U9%x9Dm#-7^=zV{GlbwSG!@oPe z!@;cB8*Lk_cc41mo6s`|qY!c>x0(K-O|mvomiC>Ub5XbT#soo%-0!$jcJZDupXEfq z!VypF$Nkf3Wj2q}F>kyUUl_eyO{Di#qsuX^_RqR`8TOiKhnX-sg=ARSyj3yU9aL*w z!e_8ftkQB=P%+bm3+41+dM0Vc{I{e@bHDAU`1g##NA$IeXP%W&N+7@WRb6WnJT7+X zps+ydz6j&`^=XTlR&derwy4wbD|L1CgfO?Iv6p}t2YsiRjsP~~LHBqZq;;%|s~Y4~ zRC;fyd?AO2mJJ)5WG^%OZKlh6feqw|j*QeFn(FL0-N(dFcjifSbyHIwI%MU&n>RMo z5{y}Vs02I-mmex{yFxQUPkJHDZoJF{P`cQB+K}{%5igMIlBz!6*+)A5f!dKmrL@E+ z%y*HhbA_K+^qqf6(WMO7n08U;uBTR8C2}%N&*O7)T27)#NlSn8b={wfTGqNR>%In8 za>+ZMpP#=O-Cs=bznPBC_)~j(i)f+1<2|EVV>cl)Y*g0hy1Nk7R_CBCQ8`2>EKGiP zecn-V5|V&c*w1u%J6LY=inwcHB%bdcP^O@si#2 z(3kkNJCzWj_0#PVcvO&V!sjfKO+fhP>bgH4f917t3DB5o^iUHq#EO zS}bu5@9b$^w#Rk%V$zu!ovJ>~8+|Mt8#AVRp&;TXCU*K~_9>8<)8L~9@HA?TEKxAZ z&Tj_|UUuEtS-_Ey9;FoFbm@CRq*!g=g_15r@;Q=TnQ;*_bt0}{aD{3mhJ~X^!^!aU z1pkxdn>bHsPj6mDHg1NXo5VctkC!U@1C3uqBBP_bD{>@>Z18F}6sg2~5y|-MWPrW+ z>xI#1Fp-~bU0*t$YCee>GHAYo!C;zk<2+WQIThLBB4>i8GaeuPCV03joF<4%_1X-5 zMnBBvHVR7S$wU`VXGM)DMnBdG$c_SWvh3SR%D z73e=ZSgbel$%*5j;Sd#Tdd1u;Tbi*#ms-!ISZeC;)>ii{XgvTq zI)MzVJZ!*~u2e}c00UN4s5i3KCR*GK9}3jq)D|@4RE`C5V zyHQS{1Pb<6XmOt$kx8r7fgv5pNMu94wlylPp3w&H$ckk3UH7hJC=ScfbniSFpqr#h zl7Q5* zdP!fJTYUSIe~;_Ou*l69Xj?O2i3=>A80 zQ>&D?uOX2PKyiv3ASR~s)OUjd@%dR*9Qd^yth6(dtu%W}cYH}dE|t*7NR*mYef(w` zZAgf3YjhU38f}lw$%S3e9&S#-gk+IK7nUYD-*HllZtdmreQvP#RKFNy{~4GdIBrp- zzLM5R76?mJ%rb1j#a)RZU{Lu+hA{qsY$j9U_fT_5az-eX_S*5zIhE{EVn+9wx;|F5oO>;9itp_&suPf3YdY!zJNodjilbLycU>NkT->^W65? zI+uipq8ht$+G`e%g?CjG90qmd3@RDWu+^@~;0FDf!Jan#m4V=Uz>l2-%B$H)E;eP6 zEuMwf#XcmXfleBn-*_LdGZCH9#JnAlUXM0t0-loc9!$mwF9z|t$ZgYfRZmZ zFn`-PG>2EiO%DemIT%Ywf9(L zJu!)FW>~K-*W-lobbm^L zO>!hnn77gAMtLKW9Z&9RIh}RTN&9Fol;o#QRmby<{ zMFkpHWhKAM))!Ow4(u~f2~^AxvnJq>ndhzcMb^f`_Nt1``}%A<4;stSTE(ECeOt&r zDfDSKOti%-Q8ZCC%7}ya$75b==ZRow>0_TE8*(mGM<9WW57mCC)G`2jyr<C_3Fm|k+n1ool1AKq7TUtFG6e=ttKpxBPZi%k*U;6X^gSayqP_=HYJ za@Pm~``)5|E^CqDMObX`z@+!PPHn%wzq`ip_Pc4hz#{|&^-}uCZJJ9DZnyLML9c$X zl(z)@{_!5&akF{Ml*zxt-hbF$(%vwWKTwR4PmR34>i`te-7VF8eXWPD_ohe)uAWMo zS~Uy%mDhWC`1xXxJi^%fJiW0Y7cBdX)a8f%=+85<^`G0+41EmgN^*+r4e0T-a@XO# zlp({GBr9)GFIH?&v%5Bu2ZiHLYDkT|=}wyU99q>jKOQtc1f)Ndapjpu_c0Q->aQ#$ z-Wi)?X26$6Zm}~2KjB&{GCFXlt$RZ(-pVOp4oc3+8oI3|4cVF5IaMM^u0 z-y2QQg;VX9;Pvse5AX2gf`D`6QUV(4NW_@&02oLp+{ib|QYlSNBwF&U9P}0^0~ueZ{4Ey(1M-qF1JyM z-M_+l*^z_K6rUN(ZY<0oME1Fil#wIcJ?s9}fmP#j=(AShsOP*vmXM}P%*Hs43CPhg zPD0?(u}zWecf{}h%qvxhiO3M}L_gK$ZP?&^eOIEkOHx5J9g*Q^oW)?JY0vT+d(mCW zSc-uAG!`Z8iv%1I2+gXqq$P`UTxMb_lSjrpdT0Z#(1d?CR{H=tRY3e<*0x{&Ya^nT!PW5N##DwA^GR7j@P-evWy}hV~4D zuy~=~%^^R((0FZ~>G(@$=<~Y0J_G%zU_Vxg<&tvf)$mu-^B8cY#~`tkj8vcG^A3J0 z^($2f)w)`K9nfpjWly{>gZWL%U_|+6GA$!D7bB^@jHHmzn6C zX&k)XQ!~piSGDSz7^{d`xB2yeMfx5NgLuF6a#^Z@a>ttNRdTQop1RikAS5 z$bq1CV+#OZ$cOini8%9PlM4n-FT3k;PkvA}UOnTY{)`f&Mq@Ag`T7hQGWu{uvfw-s zb?n^kB+`3$Tv8sX7IY#?yy@q6afh>p=6l(3Lp%Rf`NY8Md@TocKXHqcSi(WD`-A$+ z(@pQ)796p-N{_?VN^Yt`v-$0aR`97@YV|}jHz0HP2b~`Liw{A+ysyNQ#SAvdS%`lo z>7u1wU1ctM(P{-@AC0&38=M3Eu)1!6;yWct2gbVaZ9uD;TsIuW(}o<~d-V=@`wdKf z!(UxnC<)C@9ZOZC^FBA;6zI5|JKJ&U`Xq@lTKmxcCg29??jov1VRK))gTHoFA>5(u z#xQ+#n#k&cD71$e=&Q-IQ(1w%P=_HJLBRU!qF-qB+3dBai2PR~%6*V=)fmf60_$+}QGJg-gr2XsicdaOZ>#%ZD2QMnCh z8hE9=;CP4IsDcK)iVMA<&C_y^Ii&k_9Yw}f;U~uRk_p5yU2c^nBG=)56)FS#shyzF zM9>r`a?}kNK!i3vJML{1%TY)kSwnld#rAECOQ{|)hsj_mHa52EHb#MVzSb_P#Oo|> z*oD8+@bZRQcU&G7*MNaV`;LY{ThRZKvmGL2@A|g)n4*4}e(;Vz;KAxO-&QKkh|SOF z?a38xy`LElKaWtH5^x>GuDjnA>(#T%#DJo+U-Uv>EsvzrT?KL+BCrU)r?1kl0#Q&z zm6Y#?_Z^}+ndm$B_s~hqP6Rk9+i&}O{BeSoPA2wq8qqtBtB}0P;bO?tf8S3&bba=x z-~8es(@*WR`TQ$F+)-b1@2uE8?aF5duu-amsbL#Nx!x1F7~9J?rg7H*ZUc4ww0 zr5}RvM~<$}rh|3rO`iUA+hMLqugDzqBP9pJu%`RXv#^POx}eRK?@zs3*mc=|=OuXI zb|%)-`NHIWGZ_FE)KM&Ee;p%@1v?mqcxMt!Om+ zAo>%+5p7D3!+ZRTMgbpagQnp%k7YTy2pdrbeMx|0p~)dj9~0h+*mAK?fTqAXg7#&4 z*;97K8R-8N(cgJR(7~c@iVt}JqGXWMja`*`Oz`wcr$Tw|rxIfPv`z^(KJXl$?Hub1 z;dU|cgVy6IguREhV!00n z`-h1#Km?r+n^z5$jArlnMQ3$q-4D8Ar1i?^S>L;hq`QresXYf3MW&8EgK#wNjQpy~ zDX30BfeNsD<8k_~(K2Ut+rkTG~lztIdrH%`38AXc2;oeQkLIQ z;!yB*x0S8D8s~^$I+dT8SG3gH*l9=SkztK{Wy8dW(6PRh-q^`k=@Y2#);xo|tGUrkLf zuMMzk6Ut;HByintCw!P`@Zi}e^QvE8{WUxk8uK>GI|Aqjx$+g^t|#1(m{XZw%>A(T zc+BOe@ZLn=QR;HQK0~0>&*5d?i>IlI%WemYXqZ^>tTN|m3Pu8N3TeI?r{mZ_#8D01WESucRt2SspVF@H;0qrZCOd~KpVFvm@$7m-$-g(RgbQj^-o94yM0C5zIk+B zqW&2JS~6~}wQr-wd2hryg16DUl{mc|G;13N5)rHtpn{=J&OSQ;nf34ss0ro zih&S-skjp6^?6Zw)1qKMM+SX+W7aN9rGjfc;PE13vb_1cFoo3m{E+9SKSJLAOJ5Z3 z2mTj|92_79NEVM<=94)!n$SF_ypUXEzUbRS7&`ujEbcxt$jYc;Eslln!%#L^CbplY z?y@sX)hQjk-?tI?sx>IcvFDosZ^Y2xaQx0HG!_vm_MCwQh4iXJf@1q8Lqg$G&#kj* zqMPF+6zDe*m@4t@xsc$e2L03JpP$G?Q;$=nFvp3M-bF3i$1AbwZn;=_zv$qHS3X5K zm2Tp-%f}hgEMjT(to;M>x&Wm%`JF7XrcTGS+$e{~o3lQHX_4!$-gVD(Bx89mnH1he zNtP!=>=Ew|{8Bzk%3>=(sB2*ZT5yFg$AM1v&*Z`A@Kd|_8vL1&XCBdPi?_Qf<{Gp> ziVf*hLka&?8SV>(x78H#xIb*IPgkQn-Ex_VlTP)G77`Q9m! zpXp-^h9j4K!VEJvW$u+1dCVQ5=o zbzJW}Cf-`qUHEwAX_Xod!x+$ruD5iy`d7IFmCN?R+c^fbEdK3x{TOh3Gf^p@SN4Iz z*#dmj7F6RC<8(5i0%6`SUV`9N28Q7JGHep8o3|~U5|T~_B5o`oH<(*kImLJ5W^bCf zG>7(jmQF4Y6QnJW!xATsZX?Pr|DTxaDU8G8sBmk2nl|sKWar5;vwKk63;dh_USTgE zYRJfJo9w9i>*wz{4PQemnf0YLpTg2P#t{rkLC+Ae8vCjFmT9d;GR)F@@t;4ua%i=+ z?-QUid*H7J-wp**`F)L9e$b%>b9{Y@HrN;u!|^9{(KE6;reaJ@$Yi|e(rrfzz*<*; zNGWNUK!I7AbHs!Ejcc2zHfA^dE-lFLa7QA1w83YFtqQ}wcO>J|{gyVoKd)rQvex7# zIwM73$17k1!mxU4zYr9TI7IrUPk;w*af3sd(1-I}BR$O_sh4YHtJP1d9Et#MBPl(9EoH4!Eam3d+~7fD!l^CGjbeTmf`w@F zE&d(f&khcUWy8%(poy(OWw=kxP$+3fpOg9iP3eIjE*rlC+xv9m^o#ysNrx}4r09JD zY%~<49E<{U!(Rs!qCZSng}DN)H}U$01q8(4OQL)8wM+}%@te41CF?nl`h-P zK$oWX=@02m51dxm`0;Ucb3w7(vN4oIoXVduU#~0Rd{whfSrrj)7#eN(37(BetfEdD zm0Qr;-b@P_pj2YrK!yR^I}7~aVpn1eKrQE&BdpeDiX*YL`j|yC%=_B2ORGm(Yr|)K3}JV_H`sj z3-g!7R3>Fk!#o)*r&h;CdQ65NF11eoMov<0$fsED@XK~6gMW27UUE;91W3DN=gKSOL( z_%6TZ;0X-%aNmmx1L$(P^}N5#rNPpdK()mLM(>orEOt)mq$mJHcXE_|r!Sp+PHy$j zul7oHfizW{?QvJd$Nt;5qF=m)P2w7Mh1^`t+k`Nw=(kad<}+~gCing4nY)dGrRj?rYy9~I z1yw?CZgm?MzHl1y@^iNfb@Wk8FZ$`H&BSW9aCO~knHQqkEXNy&83RFsLnoV&$eq(D zXJmcy(w!?JUKbH>0>9MB3_q`O=49eRe{J)-z5U+DqHF|f>&nz@ux+09X}apyy@$F( zH{0b#0=Fp2;9|z!^kv-!X7Fuyf8S7_qqRddx;uLXVP7QL$W9|hqNfFX-FWzGt*Iav z2#dSUCG5nv;dDHbP9o?6i30ZOi)v9=A6g1Trr*`d=;osFJ?{@V4=VgO#A)3Ua2~hl zV9mqiwdspT>>*a z=ljUY?qI3lEBHx}-C>#d>Gc^w@~=paHhO`X2d`r%gsX+i<3s>vKo{aOV*bjtj}3x@ zxQ$|Q^oKs)E>EcRNqzm|cd;*mAKHly2$S;`^UGI-z9Wo8>;^t7OO?lg0nd3Z(iQkf z<0V@7`!j?bT3M?LCi?LTj?n>Sc-SY2ll|TnqPVp^ZsKdvChB8O%}!=cX>;Gd^}lKX z1d#;KNwpmxjdBjEa zp_cjjlqb1UuqD1jK!#aR)gzW54A!Bn_H&fv%=gT??}tcTrtU5@fqEx~FF(Fb;qNPN zW^B@dA^j-@(-*mYI0N2Pmf4k9VriuNEO9Kg%y=OY@3)|pWWaYsD5-XbkA*xlQfg3C zP}QLJJ)2kGf;S9n4Z3j#Ru*(y8m;C_tq`0$PXU6!9v1GDogmEQn}KN5W6{&6hZ(8noC2oqkI z9zWhK(WktD9QW5jpPI*;b~x2Y$t?CT zrwbH(KO!Ko%3lrhC!1cY8hgV~3CCuZWlCPj=;2n`IP zeCz~C`NpufT$*yRSK>n7b&1@Z_YWZNn@D}eGU1DXfu9EOSm<)nfLW1|w-zG@ z`OWkZVZ3RYe}2(*9RX-4#&+IB#|Bhe5l3VYgAv5hw9<%-GNex#(%&Y#7K_lowJca& zfk#G@att}-nO}8q{j!muNU(WJ5TV(G=tSEC8T54l)5W}>T5M%HKhWJ_2CW>u^Mm;abIaFVa> z4AqM-t-W-*4S%X^Cm~gGR9bt_x*fbTx_JXbQiUj*CyF}Vizj|b7jQ_y`)Lm3 z%-Ei-&x~8*v=XYyo9Xj@(UR`4-a@idM^QW8bOdLeST(aldvTo~uBP$Bw^pm;`3#Ax zafVci?DuWWM|W1qGafQYf^RaPM@D><{l{Sx`5z9WM|f4!byj50(VN3z^x6(+0xI32@JJBFVBFI!!eQjri^P6jzDeCC*9;w;Mh|S*+qrrN7JC$SJ@eUmhqfo~fKB>)uN2jo^{e{mlJvD~&QCrMe+=%oO`=#sUq-Lb zi1!g48f%pNGK%^Q^3cE&X02~$n2JGvQ#a;#NNVr$QycCp&D-7cp9f*9yg5T zy_EouQ(?C-_cd{L#@HNwK-Q~wAb=C}evUt6*LINrYGW`@#yyk`8D5Hji5qVd&venC z=)H})mOroGO2-+(M|B}1E^Hg*?hD?GzNir2x5aIkyw!(g1N!6(G!({QG^;^k|6 zrH_Ow0j>QNbI(#L`N=1UW!tSQXapzej}YNg1HBm{@0<*kMlB1I4Lqh}R5|y5YQ3Lm z0(8Jdbn9dyN@$kvHR*#rxZ48)^7S$~uXY>x{^L3hu_(|9TIgQoxYTMs%iYf!gND=F zj&Mi&*@)0~wmHGP--tRy zRxiKVv-7on^xV=_7YD+iV0uv2sa}3V`*Oo;gdb$^q#3l6-suCZ_PmJ_Sm#dQ$+s zzY;3q;mCq@5b7*aAL{k|o5TCzD#zP8E|BD$RklM3E4d2K>iov4^FE%)_*lIKy=6H@ z;<^37SNeU2q^YK-M@JlL*6K7=I5=`Z?zz22jOMVv6V0h3W;c@=&Pn#NV#!wU%ByDg zd&VLSAtcM7<;=DKkgNqEH&nRjf`bK+9kghb%ahKSt8E5z+f6YLt?28R%AM~fUV^No zqKX6+%&@hCjs?Pxn1QtODI|nkB|~qXsSHT&0letTKnQ92p78q}G5~9}V<9t>f!qw4 zPkuI*MZc(hm7=&M@2mL2n|{Tc9{sAlQ+?={9qRG%Nj^@I-V%H(v}KIWrx5G`gX>51 zw~|DmI0YU7&=Zk}mrK{{r>JtG^g9A>MPjuablWTXb#JjE*pt`cNMm~Sd+k|~4-%d- zf`38dcb-2hj3%!X%~l6IC%;~TT&;v?GBKHX zUvP;*^l*bq^=UGnrw4gfB3I1$BkxEacHCN@+9ZZoY8NG-Lq-iK0`V3nIExKJw38so z21iv?Ie-@Xx{bG_28ghmi$zI#uerIU{8CNRiPpiHy54ybV!GFOewTokj!sH%o}@Ls z$-=W?ssibL_no<##Wi3O+MN4;+#~=GO#Xw&U*vs3HM=U%iH}_uMcP?5az7FEdEKS?I}-Bk9!r)-0#LB|G=ZF7=eI`Nol;b0n4Fqp6Z*)Bv2@~jWnp$ zkTT=}OF3?fAnEcIk7x=>utD$b;nE>&#Y-XZDBp!Zl$?|!`fyj6X0sft1Jc&%0~11Wp2|ICac-*`c@pe7S=DvJ^A83ESBI{Mz8qpPc{b33gL zlrNwurDei@vE-fv3k{SnH>mL*+fEjnRF-~<$y6kJhpY@d(!5(QVHkiQR)*aZdXm>2 z<>P}Y`wATs{1Z5>BMhUKy1t!J+!MNnChTj6|1axH3=UQip_czHbNUC^|196T|7i0U|I+3_tY#mk zjg0*XnsIq^rJ(h*2-5&I1j2dtY|^45lURG6MeQ-1YvW2Gti1~plTj%S_*sd3Y259E zFiu>8Kz5G2^3WPC9U0b-Pe+}>_dEb=rzM&VT&F%%F61uDK(MKz|nny zkjq!`3dZShZyV*Bbzh!&;lAaN&u=XiuS5{%u~E_Ti&#WxdKuGve5-}Y`93L=fLa~R z!u*d}*oUBbPvcFv=eWSqDdtHyw(;K{$sa(OgVB%=D1~J6arwUyLc;g|s0fjNs|eug zWhUi!V6jCXdGi5`u^UUBp$v(F8PL7O=4H zS0O=UL=0A`wDiH8>0@)YHFSe{3&&ijdOGiMVrt(m85N;^y(7 zl?s|;GFVs^K3~krjZLxdY*0Hw!v=cKAhea@bexLOaGw9sIBj@^ImBz!NP`yd7bB%r z_J&%mtV#H3I4M1+Yj9csrNZmCX~~m=@nOlpgp9c*=JYB>(0J}6+RwZ0(J<>?(oaS} z^z+ugh%JbbY6)@LsRd1vP4Lum60~?@Xzr>#FHo<%-kDWl&NkLRK>2^=)1uJFvYSyR zK=y7WCHR%$?yju}OuF>p4B^<7FB#iI#Kl4i`7vm;#;c+bE7-JNuy-;a3h}qA^?v-^ zFl^`gG*_aO6ZE7GIEqeOD-$m#Ao^TKXxUU;rcUj6uq)6u46UxyGITwG!Rl*1RmVMV zchC{J>t!lA+>ug&&qKcO?Z~K-=#>D1uTHkvgmPvKJUdS~yYF>^Q9*=&dWU<@R+-gZ zIj}TmWvYKolL(-BfXu-ewmRqjclGxwK>kt#fsYXI%|hHgi-GmMOb31~!b2N0!72lX zGzVGghPJ0++q+@T2fVp2uQ=q%{A}D(sMzO+O`<_1fg8pH1L9&k{z$2~20|?F^3a}f1wM@+${Y%#apCDV?&Uq|y z0zW?2`0^LFhd7`LC~?)&4#OUYO`XS^3kVdv0=OW8G!f)cRTyM03?O5A7jW@tu=dLTr&>Q%1*Ul@06cfI@Wnb*__0C=C#+b>UFvZ* zv1(*$$Ql7)9pC~WVapvblmClr@o&B1ltqTW_XYKldI1&BVu390IrFU;6)-pZSankeX}qzF2~a=b5FJY@~@Z5zxcP^s{N8leBnqH=6GGa z+8c%NTT(NiI$PWc)_@>Qvj7K-sO9b8;hp&-c{q(k?Aph$r zR(DG|eD)t%J42f3x;NdiuCcfT+gG94Z!zKYFa-E zF&YZ|%l(gX51Y2q)!*etFiTW67M8%Y0%%K_8B#N`qR#DUn8 zarctYt>TElQD0JH0b~J)&tV}VO7F>V_ygSEB^oDP_13l%5cmx$8=0cpN&zH}5y+bo znMQYKReT9VyS`yQdFkxs*>HwzFA&kR z&Zd4FXlGM*Tz*qsu&nKS@o(h%HamD82N}JEcdUhgv<$ zio-9X%c3VNJFZ8e0TlFDkm^=hy1m%XBoA#s!?fRvi%!Al^@>eDkG^A~bHuS|jgCS5rZ*$LHo_OpO3pW>C5K z(b;4VVX-x*A-_ZI>q=^)D{_o-)FxP={0Pq6RXv?*O+E{ScS(r%n7jm=S@hhp0sr!2-2IEM|)wqFT{I!A-Pi@`rL!<&0n_{kDQ3AivaOzfn%tv=w z@~os;m>_=l(*MeKwLX%5*zEnZkR!T1#0SB>!VhCEBl&Z!*VCh8H?Cq5J^y3}d8zkx zY709;G1suw`+jR&ZGWl<^q%$L*sWP%%=%An#D0A44My@pq9e=M;B`j=9+f*Zl9BQ7nH=I*HQ1^m=w~hkGR|YYCB%y1x_I)5Qkr zzb*#z`v5dQAxfLnsIm5V<6GOtM~{T>8p3QMg<&wabBnf0ZVN$?=cT9em+w}aq>Tc9UFQz1cD$O{vae5^K;Pk)A($b9enu|-{#0lJncN90%&c!Ys#prV_L2nbW;&z>JH z5E2~|&#_{f0>GC_)JGUJBTo5~V)81BtPIfofc)JJF#p?lAcBfcWbf0Tz+imHjZV^k z4ZTJUA`&d7u1^pFN`WH)1jC*h1FpU$SbfzXlgcOC0y3;RRDn!>Qfq&RQ{C6*%uDBp z!o0+y;=>YN_{l;7hOyT-$<$~D0)b#%qU#Sb;y1r#!axR8x*UtV1=EZf&ps-Ld(h_> zVrkvwL-&t)f}wHWiLY0Put-5Djf8E=xfPNt0ZBp$3UYR{5`@kCIm=GIvk&_ zNg$5|n;;1Vx&R|I6uDu{Wonbxz==WKYNB9i&~@!VT$D&8F|>tqLbzQePea%***X#) zfhdXq{h_7vLkj_RA9I<(GQRWaJEEpmK%gv!F)I?R#{i~zlsm=s=&s+q|8HW^e%W6t zC)>(T$!~D-nUfZ+)F~ticjNUB&<4 zu}XYfp&l7F>tL#8%oR4_tr4SfYM)657n$NR;CoaG1NvzvlT@2F(>-yczN10---y(u>1NJ zeFAxOqk;aicOqT^b6+1_{!hrq$PhA%3817uY9TG;MCJ;0EF5Ljv%c9?;vZMO@!#5( z`8kBdKHD734|N-VBw-ZZ6yOp@XlLU>P#tw`ojCX1p+`%S(T}7Nqgc35jPA;j;>ne; zv!3E4sl+3;6(R&+)2AyARj}BA{w)t(Vs&n%lqyChv#+g%`_taKB@5y}hWn|agA7Wk5k_b`9{T~p=gF&ur<_?Ai z65xF)c5I0hndx_2Hu}o`Inzc)KBnpqt*k?*7voWsZf~fELx>78{s|Mk{%yQlQJ|y+ z+-iWAL-W_}l#2(oVc{5w>iVVquUddjM@ZVRZx1=xKW6ip#gT9yyM)LKIzk?ngO=(0o!|6 z$^mF$Zaem$;JVbGBfu##SdAG)@m4!eL!VOoM={1_HeyeEL-?Sn%BD0k=%NFGWt4-Gc{%?dI->Q%D6$#01Y5i@*tOKceIaAwWJC0*!d7}7$S49! zqo+niym_kWaMMZjm`}uko|Hz$ujQI-nj8==tx9D39zL#th@H^@sHpABOfsYHO3#CpB}?x15=RzX2%f#{JC96Ia^u?Ey}eoeb?dc;try0*-5IN zrD*CfHongUZ?Apx^UCK~;xqveg1#~9$M76b7=}QE?-}~&n-u|&$NmTD+7_?@a1VY> zu_81L00(YR39nqBV(f1iesSDVYP>LrXmj%SX^oG-UF|=s6C)*vqThfKW|0%`Y0v?{ zg*zQ(GxP6H$S_S?@iS))1ggi-9N2@j9D)IDVXfdT2j=%(yx{Sd1(c$D&CDzDkH|&8 z$uDJ1)5n*tb(EMI0M+h2C>k8HM2C+62Mg%p*sl2hub+3HLV=Y>-H?AEB@K3aZ>xbs zN$E2&+k65ff|P2{^#7qJ@8i_#@FwnoK&>30=Q0(|p2@~z(re>+`S%4qfSw*MXLA|> z5dc#3_l@4KC`fg6gZZ?hW~>*))NWL9` z@30Rub^zo=$Sg-h4f%V-Jds??fN+mqXm+@JXiS*>T^PnZ;75au zDO%b90eAqU_ZEWx0a-I!5qL94y#D*yM=`(1ZJ4WJc8mu8yZK|_(GgE;Zvf5FlQ1`6 zuL6AMhjFv;_ie7foRjc3A3>;CQUV0!|Am#S(gO>W{FeR4t~BruFExF;_eA`%M2@Hs z23oq892``yziWScB1(K*$HxKS)2;3;d%c+69WmJvuH&7QHIX zrYY4*ebSP`!lS+Hhk6%hg{=KOg09FkEw!XaH;ak)3mdJSzaN#{PTtyBVgPCkR z8bH${X!h!>akVa{n2mlpvCI-?x-rLu; zY=5UH{|Ln{JqJd!^5Kzm@73%M^nuhr;7y=jj+_CyD5G*LvlvlRI-tt`=oF$@s#}jY zD2NutA2fICD=it4m{9~aO_NJJfw6nU+7O-|pfLpkzca6-dkJ=|$iQ~uKBSP<`qGzW|#IH@{hzxO~O z_uXZBvA_rObbs9NT4Z~W3SdMq67c68zlh>rS+06a0kVGlU;s?cQ5smqvf}(t8VJ*m zV*P)XUX-=Y{bY7z2R_c(YI4%X?{c}-$f#7%v>8^R>T)pi+>X5|yW-#x4f{V>d+WHU zgDq~D5<#VFX^;}h1u1FiRzLx1kPwjWF3F`Oq!ADVX^;+yr9lk3m+l3DMd}&6_rCYu z=ed8r|LpDuc7Jnb=FBM=2Z}IwqX>ErD|GIi2;CR^25Lcyes|HZ zf4Y23a^8o#w@DiE_?drNHQy~!l0)ADoz?xYuV^r8P3ltoSBUMp*M01j$?a9j?gha} zWkHi^!yKJOuuDutq)|~#L!mCsI&Ii8ho0K~n`I3XD0;@oD2;=Er-amx+kp*qP)9^u z{bmZZ7C}yRGwQjb%IH&cKi#0Fx{~}Jx#4fiA3oyTJKzD?cfdjlgO>^QIPJgb4ht}t z&78>-4+Tr<0=Lr@a-U~0px)nOkQ7gR5vWJ~z0%r>RjkQS5G^1Hok*H>MSrKSucmWt zljNPur*XHC1IGy)-2ma+KVB3@;G2u~-tVyMP*6##+~y2C{F*9fve>hvTg#zUt1;?E z|4O5!4WFv_kNG6UZ3|UDsV!%i`V@f^1&lsgGB$pZF!b%)cV{a>SN_(z9N2l%=)6?m zbn;|;kOPO9HmdoDWewR?l`f~$@+mK^@0g6$H%slHq&wR3yb>!X&B*-|!=A7z!_u`4 zUoYz4+rRLBnVUyl+UW(PS^=mu%D_TPITmrsM%efFiAPkZZH*5F=w%!f^ zc)?&pX8oPiTK1TZsMtfL4ph{%uVeBzr8)_4Z@FVoW)xgRCQJfZKXc!ij?QYb4*W3vEW&o}af$&7b#72_Yxj1L1m2u?R} z4Xt3+$DtOrE)elXOtCf<&-^66!q9F4GAqwo5d-^g9NY?Lf2+P#=Eo`Bk1a8)(P zhP|^pH9Ys@xznX3USba4B30E-_8w_~58j2Jom(T;%7k54ilufW#G8}7o&ufK;}g#% ze(;p?tLJrwyy@xu<;J%!g%5mJCY^Rf9Tl&d?RV4D&Zc_73Hjt}zk>s!-&D@c7aoYP zV<)c+^2y)(C!K2JDMRPjP*7f$=97_3*wW%s{&)B+kK~!g<-|e`TCg3)Xn7n6FD5mEn4mAZ-pe%9!OfvU9|XeUi#Es?DMd; zhLfinc)}$_ImD2i)F|ZoTiwgv%qJ6GN;!?Qxv3997?D_++@hxyLF9(rn~Q7}Fy*N1 zO3|4$Fd42x^zAV3WI>GkE3}*U@os@_(qazI%6DFTeyMLp;d-Rz0E1ag-!Q7QU?Bc! zKef?QZ_H4&w$Ufyjqfb|}y_kMA?s`}DO6R_tp9aLdtvv$IQ;`z5R6ylyQ(ICD+_o zS()~u?ZM-HF-(XMHD2YkdVs{89+#KVIH#8f*u~Tq@Lwb+jL<7u{O|2(# z{)Qd2da8U1{(Ga$s5e zG)}uVcrX!B=z=So!^nHErQ}Yw?dAAOy63r^CbM2G8Ly8}%Kn((8f06&O zM(8SZMLP-dMP>6R&P-9R7JRG~s@zy(L6(>elx#r_Oh&g%um<7>??2_ACE!JyK^z?% zH)($s+2|8!EtY4nwSMz=K_OY6h`l}8+wAplvw?w?&WKdO<6z#}LZ)9QT?C6vzkX3I$Z|9{o@dbg^MiMq-ZDx0k$9p}pvSc;X{y+gAbls? zJyv=>j1dQySE(Df;`Cw*{V=g}6Jpig$Zma({S}A6(fV+GL{-%|OEXQ{KOj}tz(^93 z5x2Vh{iQSx9`AjT!Kl=L=-${+F<)mH+S+20(4eH#C6j^11b%Riy3J`&5ZnvZ(7m4u z=HYsJ44dN!B0#cGWtfDl}*1~fRE~x1jdy42f7q411W_s$V>jTSw|JoLe>dLe~ zl+ZDZ4Bw-Amc|B7437>Eb`4D(A2RH>YBpZFfQ?fs zx-8U=WU!3jnBp-}VyD&*CjHg-bJOXE@>o~s`g@nAloU}+c+zedPkkKk1>5{V91tie zzTBI1h1R}~_qE3tTt4{BT<~V-iE5E!q$BRqZ<7+cWdbuUy{RppT10PzQz+|qI)~>qIYGy3RTA>IJV-jij z;1to2jq8#1k$T+p((BsNWUGW!`z};Tv~2rUULZB2g>KKMHb)<9xN8JauIi$yr)u%g zW34N7_>kLcjJ%wUXLwjsJlw*u^g^O3yeG{MX!5z^;N{(7FyoZaOkdIL&F6-T0_cx~ z&Rm0?9(UIFtdlj5G>i5uhLI;=^t3Oe#r`(1jRYLl?z(p`Zi5wj+n{%wTghJ;l>N{C!e9E5ZB3D^vlcA;3R&s5jrPp@y7xAL) zU9yTzB7^>5%Ll=7XEm=`L9rFk@&Ri@7#>fK=>1;?78(2N2O{UX9HgUFcx1c0 z-R|Zg-}_tHPqvT=#WvAjdb@W~k4iqUpCguFHUZL2!E z(q&YF)3}b@Tw#uNT4ZoOM4{4gQ*zR$LvDTVy=-}Ij)HG1oh?0LkNWMdbsiUs{ zNO=|2`DiEOCA0Ed=)=tyD@D4C6tADmQJ3ajRoYbknB&5C5WUBWS$(p$+yTR4<{pe! z6WgAHL}MB=n~jE=e}DTDGk*+~O{Hj*%%1c1(H+^Y1Sg!=G&rI`UCdE-*nP(4aT(sFdMI!i>{H<(m1%K0U*+e?$A0K@!vG zTpT^x-E#R;X$d%u&-z}SS7I^I5oV*oo6WQ5g0X4k4bPM5G66t4^z7P_VZnwXFe}CO zoB(pwZuIL`IluA6DXktxn#usAC?o4>&Tf0%1N3I%!&|}&V}ew#Maad(CFw+C*YoTP zavFAjXk?a3!z8nJpBv_$Y&hUYM*D2VxQ(@_6V6}qOADU==Ercr`}|hE>I!8Og9+;m zOIzXJ3RpM|uaay;!!y4)a9&8qm=0GL4eWY-E6o#lkTMG9*t6@5fP0ZsFT9AP)8(VT2YghN_iK z5%b=+ZaG_G4jNccyM&-$U44)I5rotVNJ-6!BVbkUu`>48*}-UXw~1q;Yans6%?UkZ zF=QMaHJ9|g6mf0;fa#xh%8~8A$u-M=_+plSdAw65)1<{}Vx*Xy1TSNbmD=~pr_%{F zepe$hX$BlL7FJUf-<(Sw`{bAL<4#Ui5+>$7oI7{kd1Hwf-kxQC#(`My2I)V53~!;G zx15YN;@opwvE^@WwKNb&8(!=5_dWa-H|dhQ@=JKWQKex`7ndHvX>n~fy9$Er4C^7W#b`4_B5k%~W8a#-Hc#*y zT#}Q*COZ45Z)dA5?P7;~t%w-yJmXy(3$OmJdS}m|IM~VK=MVFL+^%#(;BntseQSe- zz?!1?b+5Z%wJ-#k;ub8WBfz8t=ThkmE~8u~XM+@JWdx?9r9pUM4a#h(4YS zy}oQBy>W>p2+8KH-P;k}RkijMKV@%(SjaPXDnfKx z4!4EwXqWM`WG2USW%t|jNFNE#jtBOr$)_at*UJo=E}v1Jif?}6oObn95SVUP890$$ z8dnJaZe+MuBP8-cyWEDHwaJBKx0U-~cQ%%)37RnIa^TDESRWaENe0GueW{ei;2)HeIbe_)!&xG|dtvXAt1Lmy097>4gN%;;_OsiVa4&Wl(`gON=_-OJyEA}9Ih zk$wz|_J>kjG-r&sqVp2L2hqVAXgg9n*PL`yw-3i0RWrNG2|up0=zeq52G;w`bfKu1 z*4`10gCCPHil8Vk+h;>Y>_TF(^Oxi6*mMjKrWDwDOA*4%6+<2K{m4F%l?a_N%_=~x z>GU?F?n~W57?JB{09G1M{hAz$uX(fIWd;YL1%!r$X1<_UXY~=xz;n;Qdyzqwf^paV zBZL>Z@Pl6e(=DFwUXPpicL+mXxP>75qnLf4DZf)*muJMVexH1a{4UEmlfhMX?+sBT zls3Q0RpUz_lC=u|TlRYgvUS0lUP_BY#R;2+HZQDsgLi=r6Uq1Bo8;|vZp>{n@vLa6 zylJZe;-%(&773pI(k-tXQhT84MJGq?G+l=zmt1dV_nPmMPYA>jGK{+LRu?eyUUmL* z+VGQ3>rVa_xr?<66Uh@~YLf`PiPg1A74NkOWBlq;UBoW(eQHMAG#>8fM5xN=Wh|(yf-SU{?~P8RGIiNk1ZGR!^6Xa(R~-?UonkK`>+fcyYUd|gwWgqOETcM zm&TjvQXnb%7407@2UaK-aB6w(>|~i-)DSFm+!{_-Gz@kPkfiEYCkGq$Xt!KT+OM~+ zmr6K9v)nJ*uU`iVPgCY-)`Hmn17C=f{fmnOejbXZ*(th;%dFIF`I(edGQm+uvf7LX z!qaTky<1*EP6;;P6+;bJn56|iHh#G6;zrB6CyTo~)iKW z(9II-+5U1uHh(OQO7V(KMoL&Ef6OVuo}LxSjb7zn>|zL`ky&r-ZnXY%bCcms`cLiu zO8<$0yR7tnC9VcU)42|Xg@pxk=RCc_#g*1gLpYG%LP*&~=m{>+xV{dxYY{Qk(_Ctm zO?9yjR5BP#{fX0Um}N$|VMV%lxn8dN$-|{oF;Y^mfr*49m1XIo0h}BW3p>V%1ZONA ztSN(pt{>3Cet4La=%+@GObbjT(2pZ{6Q82#-mJouia&DWT` z`cU2bTg~0ildg{_5~M8&Wq-u9dwP(+*cp6c^9AqJh$y;&Hz^PanD2Rd?Ym=)tT5D8 zuATD3$8M!zUsL55dx!CPM?6y#QWX%^FN0(!3sy6Iy+4#*?qg2%4b_mze}bKz@Q%|B z5vfI`XoQbT4>n0RSRP8yd>a;`ZKp@U*JSnTEX9e5HRjLd>b8HB`n(Tl$NqDkXIcgc z#lEFznSV6;X`RcZtB9s!!PK{E{i4eg!t77*<7?{do0oAqx32O7&5wp>Oe>(uhl-DNx zX}%f11=qELKj5-O9&hC+LtgSJcdzhh7v58keNnXR@OGIZze{m$Ku#~H(Vo)NiZ!WU zbYJZLTnNBrr<|nUr$yu8s6?IUU#;yMh}ym2K#5KyI!G^?#|l$&k(CCPW;i^~*S0Zs z!GPuT-KXHaGU1}h0$0G$95rMoSc`qZzVp2d`ZF$>o@e&>{J1OX zy6;#qzIn}CtI`msKJM_GNBaJ3Z+*PA@Qn7osIw_AHM5oPes>aYt6B1onwu*k0Dpa< z21rVapgOO=`Jsn(7};0|QnM+La}e*c^ft}Qi-6thWueICdAe!f2ID^r;Z2WyEgwi| zSs{_eMZ`LE(pINNN^UpUkt50v-KfUtzp%`rye?1-6Es|@uZ<{0sazCZyUd97dEB!If02N)t!Hw6 z+c>RpQq0}!V&4CO5>XzE{Bj&}U_G%Voc%gs$iXsMsFV&({8?1-YGoG});WK7$=SK#aPkzX{6I;vbUGsGae zl@Jzp1vESv4Q>@@9oY2De?#1qhnSC+ho;0^L<^87yy{#hJEDPQGagq8DQ(HbFww){ zx?+rM?GAMKela1~OYgQqL5f3Kgqi;AKEdTa3lNKu>h+;vUKooX+!yKXX2bZ9@58~%td|aYZMl=t3L5hvBVRI) z;2(IfTXErDMz;|25T(&Cn|Qo*+B5WcVLA#iZO^_>{dCjAXg>3B3_*KD^NhAiM>c}Q z_xdM-_ZH>YV(eCR)mji`Z(%vtLTp;BDRD)ouTx5hFQ z!F=>g-WdJO{3(|O7JWtKbt$~kD&|j4+jvlGJGY#PjrdvY|E}Hcv)A!k(`u-N=EqxC zmYB<(7tGP=-!hPFSR0{JSk*(2 zwo|Ay=`BVO=T^!Zf2Npz3y`@51a+Om)n03C97$D@BJ?`h6j45p;knOXk|)vUQ{2?< zVSlK58ja|9M;emqD?UX=+hQ*x>Eld)1=%_Ewg2d8az%oci%N@WaF@k#3TfSQ>x-qyjK%LrpE z>UbC^&N>PEGj?^TxjYC&cRm`y75XVZI4w=g)g|T>>}@34YFlLArbi61B0pQN|4CMa z@>`%!xXQX)_}L^gohDkNBmI~-rZWU$5IW3FlICd>o=)yPEQ zco(T&=_) z#qcgP5sMll&s@3V<8+`;eM#B;_Rsy(>@h_uua!prZ0G%&`edzaGjhvH}q~&0#6>fTAD$`t|!%g<2fcsQ{jdZaj`3jF`l`tsN zRA9;Y{2UpMM+JVc^ zloK_}6r=2#BO$79QKll_7?7k!f`8m}@MB#cX*rjhxR7|pZ9X{4TbdR=D?ygS+q5Lz z4oOv0lB=#L9?`Z4Q02{Y=?e)~ttmP7Ki$sSpgJ_MOrX`AF(_y?&{&E6Db>E(NM6^t z88H(+Goj5M#6#_+Y}st}gq`g9f}dPli6mXgP*mQvCk+eAy1U_}N~_=G7jvN|?+g0K zj}VPtWSvfO%2FC;zJJ+rHp$X0y@ku>Ys$gz_;z%ZlQ7NdxZ{hq>lGb*9y6lVs$5VKJS?sB@oIiKnSF3A%ksY}gKj(i$$;7$GqF1Nt zKJ&APe3)Z4EQl$QM11IWTEG0QN!`;B@iz2hzy~aTNyFIrtdNIEEgsurgBO%8~5nQIKvlaxdFxXSVfrmZ5(Y*;e1cK=+hYW{HvVy5>W4L2`bR z`m_WKA$D|$Wn^CAM44dpJ-eR|4#zD8@|b6W2gIno!^I!!8+6;{#{9C=%upLJ5B#zF zJVSMWQSX~zG<$JbZIUym=Agl!L_4==+)Dl7V(v;tp03Y-_Njhj9kX02850-1vWd=g zpgu^wO~lScxqD@so~)Rn$wVTD)V6)XR~3NFQ$GF}h~xX|oC|6>J*51i{@90i)dnlp%3 z_gKW{lOdeTUm5Jp)=)@P5cGx<8ZKQevbMv8RX&AgFtyQ%po>Q$`|{&FZgvb3a#$op?9x6^V3J0FR{2PxI)f+B1xe%!4Y#!9lw-1{BVZ`*(JMOgKq z34N-V-!YAoHElQXJ=(7q)m^>Dp<5~ijrrg4HQE*4eBY#{+}D{_UZ}Bus4(nSXj3yf zEm_w|(V!#Q^TwQd?VU#?(4BhDN%Yhz_>nXKWv)tBq!5tm7zy#d}%|hKj$XG!fsD%x4O{%xi1A(htjQ+DM9J1I(D*e%7NWe zTh#6L^fON)UOuc^e;Ph#AMTRKnIn>q-OH6Jl=6H8<|M;IkF9~hc+*?ccqu#Z-kZYa z6P=CLgMr( zus#qWhPldwbtfW5%D++Dt*xz?jKge5$;g*iCyWS!J*P{OA8>N!LNqQ5dx$^3s83Vt1^?SiG=)%I`AT9J27-Hxj`@2aO?xug!pD{~p~4Sy(hq zMVW2qQc6m^xw|k;i96~ur9fq$eJI+O{n@gu*uiHnHAA0s=%DsBKw%YL^Vx5M;a`)a z6fsB<1`uXKeehnj+CTq+O$tgUE}9L>5)-C_PJ2fp8wH*>Y?ctP9!>d~?Qxyv0COY~Di0kun^ zo`$g(cMvS-pkyMvuEtD-_}h~}GlRP@W?k2K84(dGg_IfI)63FAD!k84o_7vvJAE05?1q zxmu~n5c$uCUi{ZXvFIF6ehHc(K9)`TL zm)jopkoQpVf!bwW6KWHM)Xsb!8;O;u68P8HaK!)T07z-MPz%~m2M^Djgwllfn0*~e zw;4*;!wjFJtEtk2_1vbNUk3nN896$bE?q=Y~>-T2AwG0TCu zFY1&1*unb%u4O_jf<^p|@*mpHqXbkQQ(kJX`~2}v4wmBL2A0{gc_vy&^BkSsWbLgA zhv~5FwySswxfADz}L)Vfg!w$zY?>(9uj%d0)VrEb&)iC+7eTi!^r zD~{=u@hA$}kY~}$9PbKrODP8fJKhX*M9c08o)}G(6Q%~83>$hDaW13?5_j~y03u1= zLGo636;&0#(#RYyhCx#3*>vYH>)hsf|2sdo7)G`a1;uDZpM}+xoGK>n#`VfTvs+jO zIg$dGZYI$GjtE-T&EQXk7B~r$B3_G~(LBcOj{JxGDKB#Mqmb6lc1$1J;A>Yx~shC$eJuoz1>o5TjMx!6ebySXH( zsGBx#RuHi|XmhF0>nl6Z0hian{dI$d0YK8{3DO=ElsM}_4~vWDRcqZ;Wc?QQ*!X_f z#BG!myFV<=3k*X{pwh8jaL+yMwJFExA=M%FFJH~7V7Q0c$%rl{^<>62WEK|$LWSS& z2}<8RRPmL6{?^;?Fe6|H5;ybx6_`Rn(9<85B_iLjz!-xY;f+{1fkmo}m;}Y@8J>Xo zB}EW%zgNuQHy0x|V=P215sCe_=b102ezISw5a~n*N+kbbGZ938t4E$G&UV)}cC_b0 z^Y+l%&rA9&q4~M;P)~g;kG}r*{Q1fj5j^Rl{2OjjPL~xH@`@PpKr8KQDa~8Ff#oef zrBu#A0a>idMKp;ONr4bQ-AJp$_IwYVXzZ-1^wnK#OB{bZr&s-Xn7I8ErtrkaSp>a5 zv*nGs(764k!e}L1V)Zg?0xCMluRZ?)px*WyL!i3`8GwWo-TNM#G_%;43 zyeXx;0ZFN0lw|v7@z`_Gy?M#KDFLabFk3zts}zVuEb~v>5@Tk-vDiPgd?@+*P_khm zpqylMj}l8I%9f?yojF$$Lnu2fIzxR#A#Cn3D;(3U_n4yWO9cK;uwzI~ztV5?YdqV^ z+N)2A?1?P)H7|>H-kBLtJAAp1PW%lhih(G{7iM%WAYI*4vkX~66S(L72S-%Q@kZcJ zH&%F=9bSilUw4)nf!vpvcuR4D|9UrU7bvo;;$jntv*V8vu~!z8@NUoC&r$eY;x>w0 zyICc%DE<9ioT(8rK}0P{>O*!=|AX4ylN4b}RD}-eM~8xEGhxBkmHHSUyEF0DLmA(fUXhlIq=@uqRgA!m$OHAGlCGk} zwPpqiLPVO;lLh?pEYT4LbG-XMJG}9?!z^$)3BT(z?#~NCAL^*TfFk4H7=tv*2%*{0 zoZ)0Iwmn6XrOKRW!^FrFFWw~Hb0S*K5^rUcG32!h8z9V)wDsg5NM`~_aD-P7=*gXCI4A|A?Zc5!>u!~D`8i~-G5 zEY@$uMJj9SM;yTg^l$R;mBSclCN7(Hlo=gZQ3?#f(pyKi0|~F|WBt;_7g=DxBi+lk&qi+3 z_kIogShe=gh4Itdn{)aZ%r;Nh3!z4Y&@?vaMiCfq&7Xibz;`WlQ{xHprI6+}8rXnt!*69HBP+22AhKmpMkt={JIz z{@?kY3$n0r@Uv#RvB2l-cXl5w#nMheDXB$4oo+?^ph&Y>o2pK+rg|BMyR6w$yp-K~b`Yb+Rv2Db+m)GH=LPgrottc`E0bj2lMX6dMb8t({p@zyY^O#AMJUcA!? zYRUyyNECetD19!Rh^F$X@||*K08bjU{MLcCnQ!-IcN|zr46mq(zX=2dLI~bz;@Wk5 zO4WQg*Ln%OMsx|&9@1N;+Mv$2igka++?C|u&za6HoN||7x7j>`IGAzKisda0Y4)P;J)sJzzAdo551Io*e!DQ zLNmcq9)Mk*SWU14Cn{E0H1RYMR7MuXcujt60a@{!ilTOx_A*rvi2~# zeTBWpbAXF53b|4A&P>ePT`UQh^j>fiS?}4x^j{bhhobri<5c>@SXE$y zi^25e7X%`=y#vqPE%HX+$1fe-2aXBKYQl$3rYGTRF@?4xSVN2;Pgu@36Pu@xv|vw$ zXgp~CakKCYRWU<^m*d&HVnkQp_r{c-WD=0ncH4{mx|Fk6-$chIyc?gpcIOZg`~TqKw1 z>06kPD8CFY5+DKWaB(O4yT9Tfr%&iyr^9_cKe1zG=nq3;XHo`DQ}0m&|1T>oWj}w& zANZrl%TaxmkaHZD5^?Bgo_IS8C9wiLAJj=q?iAQ0H%YnIBqn@(@LPLCX0zLGdsoBW zwBmVEcA%|#PIErNf8>Vq3PX4dYl4Ma*ZD%zK=~6^foID;WzwxgfPxLSr}H5b%27AY zp_5qgBgA2U=f#tYzp!2QN1{O6UGec@Uh^6?e|F@DT1=p2Vg5l+mbR5hG{Qs_aB}q2lG(JP82zM(pmS;Vg%54=DkX|~WH6M;i7tGP!fOH2EMld> zOsIl!mH{^9T@ztO0}zsI<82^acSDrvBU|4c|1re# z(`e`YJ|Fqk&*X4vjIUU5LDN(p*`!%fW#YGX-z2;jgkMXhd$MUU+xp+w_PTQM|CBQl zS=M}F-u02I*wzl8Qp-Xwl6aYo+YlnSCMy7xhCX#!LJ2qA7F5!VHqamA`!RZz<(K9%u1T3 zsm9+x)RzuZa($tuaD^)ES15o)h6#Uqea zx6=Y?=#!Tc&+-czuCF<~MnZtN##r!bFL_BS4oWF>CAvv5c0#B7d;t&O$8{1AH9H_@ zZy1x0h&zTiAy5eUekuwOW4Pu?#+hlu;^xHY&eNta0bH`ffi3X=QQ20T4S^f$&+_#A zx>l3ole3d2^Wym{s}A0O%G<X;(M=ue|^+`bEUc4(;vyukPLdDi@Ih9OcUDtMC|p zO1wlaI3|HmQv3Pa2C+R=-@a$pirB11z&x7V!2s-j09W7Gy~Afp5-F4`0i8-XGEvA- zlQlVF;L63mc6c?i<1%Z(G+K1-U7VsWmp9n%_I3;JQ#Qhsqyn(y_o)>2DKXzK1-06I zmyR0Dyx|FKFXsyWZ6E;6Qn7G=Z>jy z87SH~`xXKrIL;;c&2JgC?S%^!Bt9U4kSzqMC1dDb^h-u2EIYFUxYMT%KwWlJjm;30 zMIWZoW_6Ev9lT5X%Wwgn}v*8KVgojxlI+F;3Agbx9A(O@_DhU;Us;b zsQ^(&WJ(p^elM%y6q#-&cG!rZ{BskCT#G2D;v6%;i(0I->tjd}o7LnZ>ImXAT|SHD z2REbR$$qm;u1+-nZS1Yd5Th*@*hXGQhQEx5Yn^@frsHCdTfJuNS-LaQXPy{oOsh=2 zLr{#~rH;#{+E9YsZaB!3*Wd`PikDi!`slqF*;3Ru3P4{b<>)s_;FK;$pEb!AQ$R<2 z;I4X3G**uZ6)70$(o}BfwzBpKpz%sov_RQ(Fp{IzCtdY65P?O4{5>-hzwg5i#gjuSjWjrmFAw^l+^)V zYJA;t5mr`~p)t}GcrXU|@j$Fo0vhf{93XhE)jHy z)xRZ`5{6!Nn2(yx>|tu1Fu8T}h_64Q5ej%QX-ioaG}bM=9Yg)*Ep%R+HMf^40f4QR z+O53(H8j z>wN~=hAjCd0(KRiJ%2`Z;w^6j05hFu2kxJ}BldS&*Rfai36p#;h!yXG+tWhFW(1Fi zLpx+fSN0n411<6&087M{E?gl*h7R2RJioJFVqg$%8hnom{mdchnvRe16xRPi z@h__01@Y0p09t4%lx`!pw1}XL)=)XIbbmL?`ptguk5x@I`?jr+G(#=2ztY@>N+8h{ zu4Yz`^Q<5cXG|laWq--P?xF(N&1sM;b>5L`!{i+dQIEKt?MVQb;`z|206yR@*g_sx zP)12r_n=mMMP;0oHTetWe_u>}U8 z&6m-cObXLbvf~zrqf?u_GzoL4BUszCO#6&Djp<|^D?|~C(Er2DQvX};JE{OrM8WI$ z*PlXKjA-c4#&>H>b;5)4_*{t<%`pV|E2}%hOUWA;Sc-=%`|sTup#xjOqmZ88Ef)Gg zjv|}$ux0NRMn5@1IriLli{!0Q9L3r>`o6xAIw=5Y(zt09=}WEiw_UO+S&tHw$gpY0 zpLKowt5N0uF+dOCMe^@dp%T!e-tFTjZ6_X>9(Gmcq3@SXCj_YD1C4M=g?&-`NnuNs z`pGmJE4}9I@Z+Y3kcle7TtH2FiwUJ*C9=RnYI;_)$8J{)V*aRC>mXDq>UNHuqQGTN z4_t3H(&dofr&QRdW(m_CrYgFT;E$+0v^<$~2H2obz_gbeL+LI8SHI4yli1HKvb`RTapM3;(sh#(e+AWxzsZMI*=ToZ@u4g&k+chbmnLuy3e176!8m zY7T6+2&W^W3@30o85r&ai}wB-U#t43kj%hyYWp-f zm_Ez&xf=uiZro1ZJN*swQA4 z5&ae4tz_*;o!=k?zCY-nqy%jA``=z~<|3UEmW>Eznd~Rq?GgWMc$=C#t(3EJ5+mnP z0pQi&VZRMn4~avV$o!ZbT^oN2v&}O8ul6w`oZ@cV$LS|)Hu{P7cuvX9QPW88&h7O4U7nMim1W$OiGrnOn1M;-Wj5)qo=dNe2+r_d`;{ zGjYufKE$qDD*wMT4byyxpmvD0Z(^rJBtBibiN*olGs5e`6x2=P=szct4z+)45e$ht z6*pg2*lNH3ciQU|{!u)9j(0Sqdtgc5PV!ivsBfYLKmO|Jx_}JPwX`YT06cvxlr8t=rSECP3)f@g9}SM6Xe1yC>^28o1_v z{|%(%UDV7fe4aZBsi;>ij=K@lAq(Xefd9hIw7gc6>}i;{j7CC&!0(AH*~QI%MngQB z=Stq)0U5f_qOp8+HzQw4MqX<18U8oAl}j$v)>W4Kukh9cj`|t#0N%w-F9;aE2q27S zXvbtGRwu9l>%?O>9=~-63&9ZdlM$rPU`XK#%bfmK)#wK?MplQXX$BH>a*hw+=6nJ> zp#UHN)2;(v8rBDz@Hk2(UQ zii!5Yx|tZFA`;1$3(?2t3bn3Xf?(M`1||fMK0gZkLGFtM__LywTIZ)qH;Yyl)#3_h zX(w(R;Hh>0K|da+r~d@;4tWRg6%VTLa3QHT34;bF9Udk&bOhk>h2}G%W`5wy6Q>si zTCPB{M>8}WAT9Ebq=^ndFaLl1Sgjgol9QFxN`LA2&$sb?x)~^g9uk6Vx^Wfq2KBJ_ zSTKOkW{dfI8;-#Tg9`p6f2U`#qJ>PCZe~J{++|X)V{a|^mE0=3wC5^L zoM-)EDQ}uWS>{LA&^isLjLzl%%xciUOF{oKJ^T+hg+EGFkHuNi*`qA6)mu^(tU>nV8OqlNci74?R5 zg@@hj=>!+7-o4rMK<$Ue?@!E23{;hAnA}_dZ|IJOo+V1%ztQV2 zkn2l;>RlV$?3B0+d2j0kpgLu_p#X5##xTGGd|%(P4z7??Bsf zhk_(Oe+urg>ull@_jIbbRts!o!9T|Chlhl0kzk0`g0%&l{};{4D6sa)!P+;MPVkHG z5C)ws$8H72WMWX27oS*>_8;>d;Bsm__I+ODi;0H87ee~xl}uS~lfQj^0H@FqKB%iv zffjWKEwp?ObK5_CU`pK&)q|;o)$pi(Y3zBWxhfs&t|)mwsi?&DLm@%N6xKumet|KU znE~HsSF+w1-*^Y4(07mw?B@Fg9<+1PX%Ahx{Ag-$ZPG>be>i*Vs4BO&T^JDsL>dGH zN$CdZ7NikrkaSVf-K{i>ZYdFwZYk+nNF$vRi;!l~%{SS5zwf($XPk4!8Q&QGv4+62 zX590hcU;%?K!n*lpnGDYAGA)f^Cws46rQXuUJ_{;%`Q>t9j*tLL-eNP%r1N;6}w9( zaJ?;T@ZVls<=R$SnQu|9RlOy*@AAAO(oyfr zaC-zNMv@@jmhu6p>0#J{k+-{lSzToA@SKG^*cB(nY?NxmjSBKE#GirN_65V;Fm2U5QcL%M9#-YktKbjT5MgW z>shfsUF11SgKBnQS4>e z$0bu$x;HhIEe}NzZm{NJbooJ)AgE5tNO7>-?0dB{PfpmK$b6u2Qf)1zGrQE;_L{!A zaW}?OX~u8$er}2`L<|0hVt&fkU3;Lc|7hX&j^?WVN(|Jc!0xxucGt(WABmyK z$2AvqdI=`F<1*nq%Nmn7VGF}Ms>j0%g%o24MZ~qd1aP$+ z&}W#fGzQJ*8gPO}T{i{x`N6@8#@BWwy@LXU#v)L_Wyb-2sHO$QevGx0v@lQl9eGe} zM=ITIO)l)J2^_SSv$Bw@=Gq?#S0XN&HE|<`m)-BIZHXWC#xx)HS{4@%cGB;bb^qqQ z4UXnZ*9#@pBrF&Z+MAmS+og)egI7g7BX&@zp);zIr7K5|oM)p=fAvJc^^{#qJuKd` zQeBL^3|og<<8tooLJX_m(^{Os)!e#SHh-UB(kg4}h)c$6Jr_R_TYa@4I5YR~-cjLW z%cAKlITn|JGOy1PUDVxvnTFwgQGIG8({a}ZF%}Y&pHXiwRnt80i+`<=y?vafgMJ1h zClYlqY+~X&SRhJRUVi0KXg8hmg1zI5$w8Tct=h67@6grmtJSmCIctqH{gq}kyB-#z z`pZM2nU`#Yv18m3+wLnDBmibuS?g!-AYeu?jmYB@(wHU?_=X3s$Kn=sczLhc*(@w> zPV2XBo+UJdIhWebdd}Chx=`iDwc~T#)gHyum->0k?@M2KW(f$9E(n)BJ|{7wEe`4~ z?<-2VKFD>YEfK4Gj=jKNt6oClF~4S}4l1Ij0IJ|^!&Tn>C@dC4_s0 z`;s@o%6gDg7ZjncBSD4=3Ll_dS@geeSKf7OmGwDqW@h)`Jw$vDm98F`rw2MIi*d%b zV1dZvJ*ga&V%MeU$M`fB_Z&Gz^ZD7kMo-GRnllkCE#_E{E|KFYZYJ0(Z38#eY`lcp zrip-36;8HX?TNh&HJxedgchfF0&9sg4nt}B@>d2w7>Eo-i%irSw?&1-UG0<_=SIwiX7aQ>L+3UFj?}%_+%Wnz~ zPp~s>V8*(hi7d@q_~Yeo70=oi0GH#SAWN>{!j_(`p-MAS_r_7OuAwbVOkhdE5+92S`;FJ4P_$ZsnXz{el(S_{1R`kNo;o?)g{(3Rqa$I3N% z*BgaM2M;x)fdg;viv;R%<@Fd;pAPb4>9$!+Np~YPqKn|B@LwQ~lJqFW1m3G%*F|zO zGTu+9iMYd-!uL|X0dI@qZCtcnmSMO)E6pbU!)NzxkJkblB`8xD7n<@Fn*#O?m3f&5 z`7-rFF^JX*m9d5Eo3OJ4s?qWWdSOltm-~N`3>4YFF9D>xv2-@(Y4dpQ+Doo}+8o2m z@m2GKwV=NH)j9>mwI<{;`0$MRrnB)3Y-#nAS7D$$P9C!+^T-)@)3+j%N>#tdrNu<6 zeSYmW^`~o*cPF)+OmSCTg0#oQdGgqMlg7RzHfbF>1a3|avGPdZVde7}Byh2#`}Zq+ zB?OM@`zKp{>?DZZpDY-`Nl{_1ui>n3=u1}hxF;KzM`BgJ^ZM>MPDrz05_4ag9` zJwY(1E>?x_6drZ?T_2YBHe-?Dh9MxRw(j}&_INOh6c^HzncE|a!pn0^i5Jeg-(8&s zt@3->*7UL%wB#Au?CQiBs~p$lPMo3MYHtk*lM}+VerrYRvvx2=2*2sR$W}h@I=d~l z(?B+;{GMwPZsK3IFdJ%+~}ELOfF>z3`O?5}$YUZ@FUWjoFgyEOcakGqUp zrV5(p6k;lo79PCE!5`2pVmJJnN~IGS&yPAH3-x5d)#MtcALf_HW5ZRz;_bmL(TSM1 zbN*7jP$CEItNY5*J7PgnXD?BTcTMQQtC1*NiX`1R)1WL1%M+fVn3>7Ocyaw1&-dB zU%iK1bw#G_w_DkDFR@3Qu)TsR_p4LH?@}*lJh^;d^TSx!Nq#|Kj6-@s1X1q z&Bwro#t7ii!pC^LHuF3dxaan5Bt-z7fY6G7c;hkjBs9N&7gb1feW@i>j##8@_Hz(e zOPE5WY0V&}k`M!YGyvv7w<75_9u;+2E3R7Ex2^O0>&TV%gi zI1LX@H0Nrh2vxw`JG2$~9Jtv@LkTPM`Fw8{3zHGS^zxVg7rZ33@(Qb`|MZ4krNT*bv3evxIM3Hi6|QKUWXa?wBF22 z)5g4{Q04X-UX|3%BK7ml!(iXt5rbIBQyi>)Oi+nc?8{T7z7_G6{s?V5C>jHPTaY7GaePpMqg^6lV6z|GH>8*u|uVyDOKro zA9AJUXF-hrJuaK|;H+n-Ng`L>j8I^|t>J1!2P-xQL;~tIOy^xPAodz37-xy&c^ zf;duTHr461N(&Ps$=4e)mCd&mC0gjyG@W(mXPW}Kbtj!KHSku$Y?>q{T|FD!9c8j+ z-gEPHeBs3*tQXFkx@}@wC?xhdV84lDz{lPxfrv3>$5|XzvIlv={t`8z_RK!Vf&`C4g1Vlb*#?b60=g^%#;sLWjaC#aBPjAqu*2T( z7N=@+_(gd}LWJival6xA--XJ+ZA?gkEK?c9)Ijo|HdQdlMDdV+g(KFIc(-Soi0(^D zp0$$XQPK+u#r4sZ*c3(}u3ePswwAi{Runi(eK!8FkIM6Jp)v*2EYm+)ZtQdM8n*6Jo6)Ln-&$@W02_fE-FjIX?udElw~K82;VvXkQx zhx^j=lq>6o7vh|3Vr&$9e3(mK%C%6rJq4omrGX#!s#Wqr7mNwv>jQnTA@(x$>1C_k z0=46cRel@IBqrACg z$&dGxA|(r7*B;U4sy!U1`%I_4Hfw*XF>a|yeMH~uUSE@-Qomlfk^i%H8Bx+SwXj5% zMmYWFfIIIA^Y5%{-T0Z-BT)a)Ku`WBDORkX=>QSWm7|2t!%~p~annk%mf5TUwibfE zZ>Gy`6lvdJ3@#J7#XWmsyhIhwcx$3 zS3$w8Nxcv~`8-+l#2i5InMr2sI(x2pkJEROG~;ZmLB#mOMY@N;*@;JP;TX(M*i+U9 z=R6Gm5ugdalCKF{Oz<4;R_;?Lb-i(MI>m+P@g8}XImpVbX>eDnuhouaCkake)-Ug9 zsTbWW*pR8Fu2!agO|ZvhyqwSbq)`w9%%GBSZH6@WVIAfkFXlVtt@4?Er*|H~Ol1eZ z#KsvpuYSP~6C3Te9o}8<`d%I6^3s;C>UrcCId$NixbUlF8f#3CtG->GX&HkDZYn0? zvbucF-K+79_PmBpPZvs`nYu;Yst_KvLw4V@I>at$%`f;^Opb*VxVlr6B(?o`pGn0!?VdaI)yv!G?WbZ!dP=1HB^8QAq>cs=1bX1tt-8Lc(8r zfBt-mgU)VOBizu}a{eur3$~ikZ_|Knv|DUR*782{q~*24`wd4gY3>pFpGR`-`cj_u zbeDDZSq#%M(t7q@aShWJ{8BvQ8rqfa0hV-WE*Fgk?v3Pi`T#7+1;COp_gb+z{&PX} zugoBfaL(|E61oEc!(Sy`OaIdqR|c*1DDkfl?@=#Z%IRf%>~MsA0{qfbOPRvJXG;WN zC1!Dn-U{12aov7)Y6Hw+{R0R)iYW9|qavQHE=Q9JAx=*{#~wl*W#zy+y^Xrd>|r18 z7b*sirQK{!)AGCdVzJQ0*t&W%mNLT>UvYPu6C9PCOK-7pQ<0Px`%2OcP^-C&0j$kZrA5M?OAGCamedj z1(Vdxmf@#k1(l?6sXn`nWLF20iJ)wQWBArh)|62}`V^bJK-{86L+(9po9ee!dO%_w zU|wbD|Gt>LW%(x6N^MeL%k6UfZ17WjNd{xnH`pzWWFaai+YPQ-DODhMFJiQ&j_H#c2g;P7HM#@)?5YFhOuz=?G3fm%gSb{aKq|KB<#k~ zlI77+T!20vvc2Sf7!s_J!W@+r&CDjGtV~6r#zJ1^_M%86CUKV?13!IrxAW(Fx=Z`T z>R*Z{OR>0*u_E)KZnZL-HIXfh&WZt%nR;qGmI%QCxERX!a0d@*ANYV$p>yvF=u-(C>P(J7`FQO*PDD_b9HW!x zFr^NEMGQ$RFCb0WxqGpQ12D1ED^Ag{#Ib~=gJ*FYET-JZ$m*XS7~qGo*2Hf#5a;^*ib-MTpu@-Z`)NbBrSZC8e+=eR za{>X%Tc2`csj%T6Y{_{2jw}pL3=&Ow10)1#jdY~i;jIe`qFLM*Cgz2 z4b?hkHPw;2lBaXkiPA_!VeG5;Trpe1+nK3?sZTFXanSWG_BIfD`e1f(hB$w&H$j#<(Ayvv-Smi&h^#RRQj0`~=a|GK&qk4>?|JD}Ny+BMEM{yd-)7mXf9KZ&obXVR$p0_C-CJwbAo zA8e`sAUWS0Wl8=Q6_F!MoPm?g^ySULpQ?NtVY0Ee@`bV%iOvELRv4fJ-pQs@dSk2& zwaw{pfAj^Qgcd@&EVEcVHA&mD?B8c26mrs1oP#{#{|7=~AU=G{&=*rxIw-^!T^4qU zuCQi&zI>h{R3*sp2Ec9CLHzj-2>T;3U)sR?z3sLv3ZI8N8S@h6bIz-)GFKAH8u=AZ z;Fs8A?^DkE^pR# zftK(kPPvV2CtjkWne9@6EC2)uAWi!D(_V_uK6pM@XEZ!Ee#8GN1KV59yy7aQ@Cq|t z1P|Fz_4T8jEhCFF2X%+T=Xm^2@47G8|8!seFk@8>1kMGeG6)OsIU5;JT)1Lp8|U~f zaF9Rehy_?JHk;dOgwoORgFOtH@E+QjwD_Q_IkNr?mB`iZ0Z9)zm7o5A?vX|RFRbxP zQ)mPWuAJ)Z6!daZ^ue@0m;H*F`4WtoUGmjN>`T7!Mc7*?-!t1bFWs7Vp2sg(K8a!T zrtv%nfDNDGui<*kqRqk-cO|blvfGD!l^X}2MVRS27i1x6697LMo* z?QRM|utT&BB7A5=S}Q^kKZ2#L7b-|SJnH%3V;Z>FpmhQ<5pF{Q7domwBF!Qz#LI;5qiTqiKeu5i16{BBqd=Pg>Q^mVL@t!w@8jomC@l) zh#m#=W{1Kdh$6czC0G2O5x+?zCo+9&4_Xk_7JGNMV&{oL^^#+8RBj^4*~z)7{gh(e z>a=F#r1Q$)LB)?JmvUGE@4Lsa*mJ!mikD&)Q^OApj-6-2Lr-h%P0la4Wmx>LX-wQQ| zXe@c@&dA{rk1zxJ%&Luil?1qUG__fL1=wCw@0*57NZ~y1az`nr>Ciu%`8EWCoS}X9>44*UjRt7&{ z`h#Ywa)x@S)B38>&ZUS?Lv83|M_CB6*Qx!x6Eu zxYo1vsDM)~K?Hgqu(T4@EYK_49I8-}mgTffNR_uvzsD)M#l8Ce?POTf?Ox0p(n_ZC ze9p7J$ftHEFAgV+J7hcSIu|B>1=R92p)&Rn^8>U9f7MOOow1Yt%ht(d=6H*9ztf^eP3 z`4KG4&|3*g%E-k!nb<{Lfqpw%-8s5m6*(TnY|+?x{Xg5=4<0AB?IO zQGT=3go3s59vORECFtD7TXH#+kA|H-4dGloTCRj2y!m9@7Sorf_HYYkNNN$Eh1oj3qj|l;_EV}2eVW+P@r&0X z(%kipmm+x&4Nd`Zvh3x>!*Z8yHfNX1(?_8C{QD}Xfvd}%|D{?BppE^_Qs)Ts*W_f& z6FJ##*H14S9Iat8h12CDic^adiql)@lwFa5mK&a%4fTZ<4&DsR}1P3Mm z#YPW-p935nmb}?+Qy;akA1~h2Hk8*6gkd~cS+2?>%;xAr4(SSHY0yD;#+?AAJkl+< zQyQ^Rs9mO3@Ti$-&mo3qNYmMT(az=!b0HU^KqE+7b;-ZWT9xM~_G$h8@EW;rb(2=u zahASjKox-C_icUJcWr7xi5Qb@*;t`&bTvY9Yx)(IB+6&o^5qf8xgni9uYw1lifJT~ z`me7pb3)3?e7wc(DSe(|fFQr^0?JUVg$7?o34cUgZ>RF$8qQks(umRHA8?Ml`?0tz zFDOq3;!AqH+w|gIPVqoS52P{T(pl;g#A@EowbVxyb!A=ea#WkPci4Bq!$FSUdln+C z_vhJBU!OLGX2b(-^VcDu9y3HwN`490os^{H4H>p3-rPSTKN&wW|8dGk2NTl`Sj%g$ zUpz^2crMz?y&YuUdfzJZ!Kj$ZhYhVn22R@^4oTi!XKasO=h75UZ)c*G%!tgq3kh=9 zpM1Vyw|Vbgu5_&7X)9{xS3~v@^EFvx)q^+-wrhf{Bn+o-;N4Jo+pI%waG;-JqB+fZ zIJ&wyH`DNT7~x5wXl8UVo?CePty@sV&yaNPJ^{SPpygMnLzvDkMXl|^gR-Frc(m^0 zhWVuJ?Ds!J6$<`paH-%I5%V0RD$?nY1q=}@-jK)lp*(k06eQgVga{|S0N}>20s+7& ze7-pRVP0;$*G6ec4SN-nHv@3BmfVFt!u0Cq%Ms>JjNdjZnL=C2%{McCm+5Nc_ZE!- zrl03LrSzPrULyw)cldVQ@wn@iu@(DZYSzc0^tXYV$Z2@Sg(jwR0PK~wt(Bvyw>r7NK<7O8;3Q2UaFyVZl z4mIO_VyAT1tH(TfM~b=$5ZA!^LJa>R99BF{=H-rt{#Z1 zOkd|nvn(t6uB2GJVQSbZMR7-|$R+j8ryT%40BolbsQ(zI=y@k00bDTiwBjk;Fu&EP z3BPIf`t<$g{=NCM3x~5TeWog}3m)1O4 zTE8zK*jQIL9C|+FW1+*%6^b^?I=AAaOj<;c3!dCR3*3Pxlb-K-zJFdh(&O*7=4YVx zHl{H1`9ZcyM6kFx&-&i|GbYLCcFOhiBZ7>$kN5rFON{=&dZwn7HT;DTE=?=-H)#1^ zL<1=%7;11ZV!c{9r?fyIe~(!+D}+#CEu<+k_9<{$7Bvvkt=bCE*g-q z1MT3$_4P%*vhVO;tWCs-v;7UVM*!CJve7^&pLc@*oecm=TzxORi!RO`2uaQ+JaX^w zRt?J400(g|u#l7OK(F@)zqQ%be(DozOJXU5;0IzqzLJ^u4{EMAm*-*MI(e8%L9UX_ zb@x}IL>~(_F_r$3=d5_p^E-+X=QY&L)de0|%NI{}C+!0My{EQEJXmim6ZnQr>?@P+>z<41x8hO8#CmO&-=R(N0TALSmij;9Q4Rt=~WU&Gvq$P3^)w&n|fZtuKh$)S0Yt3`Ivc8t)aARr& z130+=rNxy7Z-NQ#3q1ljbzW+g$$V+129esQ zl@=fHF-*Z`&rr{xpc2+{-1)Atk^MXI?Vn2TId4nc%^&G!;|ex~FCxem8uA|(fr2YG zsbIOSLK)ubTW%QbZ&C`&POnJG?(bP@D<&S&Aok6r0FpZ8KI`P|fc^rIkdy&Ac=-&> zUFdzuT*i9cTP7^VR|`zT=dNO(vbLW_XlC*NF72QAi=`YA_&5ct^tGf-ep6q|#>sbV zqIbWw@|O1T^`|cTB+ChP-W(Qisr%H)nucKW3268@Ry4Fwh*oHO=t`p;;bsq_wo z?(j+Ftw+gbPM3T+;E9FW$rNZ*_vM!}XJ_i{h!61OzIV@m22`Zlx&MrFjvt{%{-xGY zlmVnt?qE|1Q^6=u1%@`q~urZ!Qisufh=UJk#Z*b>02;eBu_9SVO2A)(;Ij z4{_+;p8fI!=(p7l+w=*0r zO1oVcG`E3Kk>kh3)unIeMfr4bex{~?r_BU$G|z(e zah}WJfIsmm#-8igybu#iOgq%$mJ>51Yf}?4hf2Z3&fK2PuJPG!9Y?Q;JRv=`DA09h zzJx(Lc83U_ngh8?l!IKk02ww+KJ(w&!J9m^f2M|uEMv`wdRqf#(|#R5{YIt9djN0h zPH{*>7BH(hKwC@xuB}CjQ$n1KYd2kujeSj{?Ho$bg6QpwNy{+RW5|gGh(X^+IppF= z1JJT8-4EET1Kl&HUiN#s`ut;X)t6V5v>w_z8DUx{*o*TpD(~ZMyz~}IqT5SIQ6W75 z2a7qq#Vys*$+z^0s58)8V$a2U4OQ?+ylwpaxE{-II$;+m zGhbJa2ZNvB?70V^gX6vT#qkau>Sp~1)PeRShGI~~>gvd-H?n5uDb$UD5pCu=^wV1c z$t|kr$%uhHTM4VnSQk+(LuZ4|3X>_D6rK`dfmAG7oco3&DlDt-7S)0e*@KY@+Eg9S;dmxb8h^-#Xh7uRESjZ z>DG8B1e~iY?a4POhk!?c^bMpcVK%a@btqAJJ_F>qjGEvn*7&-tu<$yQXMt;P(*R?z9N*pVg4-xy8xuBPag4*1V<^ zHdx&Dn9gS`hfhoj?oTCURt)fgj?2brUJ)~JbmnfyOLluMMVOqom+f5XTr>(cN%h9y z&0Dbu8n$Wgst2HzR1nE)N;BO2j@>|r1F3d0PpW>5!_Qlny{qnZlIC-WFwVj6Yt#z! zg@DhvK$61bC5iON9T~{y5pgR6CFZ+)43&R zYiOQl%LpwVBByG&YGmncMk-u874Wd7u8Y=2#~0v(Di5GN8-(8>ngM|_yOa01ksfk4 z6U=mFsOjQ~!Au<^T6;JxhJDwjQzVAj5lsdDs=HBE(2up(-%lTR+E@eRq@zgI_RivQ z>yD$bT{61c_RpwUmuUx%dp$EZKP$bxy*)&k4z2gyeab&60f;mL;2PZBo%o*q&`(i= zbSu#lEp68e?6;#

wo--HcnfG$X=t71Zd#@1b5;*j4}F31LheH8+RP=J3E)=lpKP z8AU5uu+8Gp0kSEIzUFl4>nFuIXZhG46Xks&ifQO~c-lj=JWu_~4)Z6|2mrN21PDwQ zLc_Zr*R5uEo2lj+eSLDiWWaY9i}f*aJ6wOnobpV`RmW@E&B2A^8N;(X-2JwqD(o3y z_##e?nFe5owp`3c*a6=ED9wqL=1G|T-=&LOyg}-Adh!FiY3r(+p`8?8m#{*{-}~aE zo4ZzO15E7f>O8Qt`?>1gH-~+Lecjva>a9t}67xPKg;(;6F_AB*ckePJmo)c-xQ4*M2N<&1T4iC55^ zKNpZY%B2!(rNIgEjCWN?#~`Vmr%KjJG-T|p%-=q2t>?TCi)zOU)r2Z^XXxusRamhX zYcjtU*a$42{50pEt4=Kq!+$Ie>h?(S33ODd(7YZg8Ggn1@xbj=tk7aCm(X?c;%Azq z_GUvp;uDWi0T3<6uT0E?st6X)NUK9^NhUFr)Kp3ovN?pMT6z8r-yTEvGP5q1QKVjb@e_ZDLV8tx_WvS2D|4=&aCkF z+k1EuJ9}_`8phWUrbEI?LuZQ~OC@hH2v%C=FgEHef}p#YXe-5FzPyYcp)36CuBwPe z7C|gFf%LcD$s`SM6(WQ3o-irEk~=J2S5dHW=+RCtb{_i7$1v7!;YyJ4si-&KBw^Nm zKk5mYXQSFshg{#(--0`UB=v6(3Sop@qK4St&gA{F&!2#m@g0^ZSph zRe_U@;M1(&{<_Q1=c)!iv~X*ffZc?%WWX0(;*$qdZ`~j5Kh~>*8jTxjr0&3t4MG~_^zu1ZoNU~xc`i{CLnsU+htbmK2}*D z`gja-*yx2YsJof_!ZGhhK;}5qy2RyL@1VMv+!~L^DDOUSsaW8EQsGlY$48hd^2I4T zW+1rVj~Wi$ar z188undvLIDd^co{D0ljXrDf-`>+b<1!sA516)zMUOfejBR$N?R@V`8o78dmLznIEx zseDyWCk-7)`st<(-YlSY)itk&{EGF*1uluyVS>0S^UgPUxQ;OM0^jt>E-DEH4=94& zt;!atV*Wb@&4lRX3t#qGY|z4OG)TJ&DQCiQ_=YlPWJ#Vk-rFh@z(-cnL~GRp{&X za+LSL4%{tqKcM&i>t@FTOFZZkEgYnOx%EL@-|?h?e`{s+M-U*F%@({fdTyi*?yler z!H1=4;DcaB%8SPl5MfOE3`x2Gez5R&qDOc@F9bQ|FWEw{8+ft_#CsIPcXsg-C>v2~ zTWMxR;3u&R(}ISc{%een)b}LscI@bg)BPF9s3w=bezlMV`fY*;GG3q+^ViuYOHOtM zG`oMrj4`0w5r|dmx_DEqoZV0>hBg(cWe^M`GPbaQo7nvpcrW(L z2U~ztxI^~$Q=|L4hNq9`Y)^^5rxz=Rl9jPA|00wH16D%)NEMJk2?~ICqdz77-02)f z(a{V@W)mwPK4@Kv7bpAs%6O-vwEe%FkYC`0G}ZcG-&_nGOAIr~N7#1|CeyIfEAVju zWicS{7l@(?m{;z-inOR-?qUSNRJ34Vux!ZxH%gZ{S7+U@=W)p?VoZVbW2>A%;?`tw z@U->FgExO`)=&l{+>vNiM|;}-+||Ib=mMx1z|-~60Hdq|pbbGpzQqHYwhvSQ7WFfN z0FMJFz$DB;;;6sRZZVN>0rN}sa@~2|a^<13L<~faXtb45mNFMiGjGhk6On-Qp){L8lD7-2y(3t8T{X|+38$nI-}aEA?nJlbbWUG5`! z{`X?Udx57@u4i2JkGm72=mEk+(YQu1exf_IjAC%)fF`ANB|(b%u2Ps09qh1=pNU4SSfQy68v(< z8|3-R&1jb~f&~p#h05HE%!~04K>H&B#VD0z-Kq228U3@dh5f~?Dqe<41>&K=fMHRDnXu^90aPed%xrc^-2|YZ`@o68MR7x4x9t2Alcdf9l2mt%ev5 z*xSQ%Ihnr%32B$Q$Zz;K}Wfisw)^Fw{4PFaO_#1}_!q z;xYa0jyii5Xm;35ywnsu(RZH@uTQyU4XsCYxTq)pP3)Bur$9MkfCcrdvDt1 zSe!3=)Eyig;y;Xn2*1nspu&?S1*>7fJx=!i12AiPv={97NnrW4rS9n2I8=Y_CR(!M z*8$)raDaFFCk;Acf(89q$n?hDvz>j0ate))>fUrxRzS1=TT(@qvgGcNj5|A(qu)6U zq@Rs}bl{6Vaa7;}i$?j&1Jh`;{d$oL9tq?CfzIol!yK+6PHqu9y}Fj$7Ma39Ia{t$ zZbN--fa~@LBn>kC7%}2AcL$|`aorz&cTj*Fa+BHJbN@#T59r|~%YPpLzCGq&!)1X5 zS=uci?do0h(6%^9hEd`bWoxj_+59hrOK^PFa?>w zh)7TpeF5wnu-eKMqjJ$fB7P?XD^k2-;#alPY@SFp8P!Onb2-c;drjv&CraD zjHpafF^xxU^^jNQpIDlmris(0g^`JwvI!lpnV$t+B-qw1p{S?V{s`nK`&bUbpP9MpWoLSzAVG;(W8q$g3D z3hAC78bJiYgsZ?^INM!VY=f_?-JJbPL4mx*>K8hmF-;+#C!%w^lX^l`)7w)rf*DCO zwknX>^(NT}UgmTpNMk#yc<2ar1o4mw*Hew$U)}V5 z3Z9zt)foiZhasSudkMCBA;fM?!!OY8QJ^OPN!%WuNgD|fQP$47=%8=R3uu%uA4w${ zH5{~fVrA?+jq4sJ%ZK;Sv|l4}4u(5Khj832ZG0F4fP}8+=jVTzkG>HYuz!p~iKB`f zMJ(o%v}vW9O$2|qv`6mw((UFZ8}Cz*7RxUeEyF?+>LsSt@-ReX#t3GpgO-(|G0uJ( z7(CzDr$mliKd2P+QXlgo0{LF2J3%3FBpD9;S3Zr7IHe*Apu4|_ZBaJwa!-+m%d->{ zX9>WryliqShU)d$L=M-meCQx^%$46U0_<_hm7t`SZdWG~^S60<@<^H9b5EOA4d`XjxIsbB#N75TakKSQB`{PJC)B(31A`Ppy38YXijd;=kdLFQM=jE2}0E)5JO{=T1pCx>oGfyq?j*N(sfJl zzOE+9EJ@CA31@y6A^}~OrZ6(Mbqto_@~jXl6{?-w=(5#P{fdhw-b#<$Q|H`%m2S{b zY^cPe+OFp$V`sm-)|MtJtd7RHt5&fA(?Sk)FmxE5ra}vZbk%X&^)ti|GK?mv?AY-1 zigr-*K4e3&roqvDrgm}gy;8wRUNhB3y&v-E5UJN*6r-QDIn<%kz22Dd4X3T|5-I1d z@Qz@{#P09(%&Cm=s9osDv|hb{JsGfL$^dohZyJYjg4wjC=Ps&DsfMhgH$6l5g0(d# z^)J1SOR}z18&0c}$^4FU%Z4rJ$uu-cLj)NJAi~MgoL~(+;7NJ|9`@8D8!w}g~KFD0oqhaeYbV^u`!8@-5 zjJMD}>s+oM8R6V^islJBeM}QHUEFe2%31g0J#s{h!)2Kx#C->e)p#>iN~!YPee@8& zN>1_6X*zSRDdp8{I{6`ub6FSSoppx_V^?2Tiw#(;3zoeHF-X_F>aPsHVtBbSKOQ@RN7_rTcFIT>F5y`e*HoCa4BHTxI~AYr7Q^Jcqh z*pDFQG_Yj^hI)g`aCPt^>zSOfCX1k5kKlg&zRRrdFVR|yqUQ5>!=%>YvZC>0Fs}%r z7emC3F_5dzTcw8~-B+84b_xckdg~ji3|R!pWC}jjB5gp$)^0vhboT+Dz*^Nb&B)OB zGM#c~UGn(1ozK8+exz1zfup0)!Xnn8#znAaxS59E*Fqh*4NdNmetd$QO>4hs;Nz^} zF1#j-s5oE#BZQF=WNU`e9NLTn2mU<7)dqGH60;1-w9nhzkABousSI(`9t5oOo><%yU zp+1=^ap@@c(A=eswqafMVZTyvh7A1%oOfFXA}RO;LLG+Wl{)U>i9=D*vPu=uz^-CA z!9I|=Et4d|&)tXxW%eIR!dOw-Wm~5`*jug*41A;6KBtI>UUHg0euVp%kC?)cf+qK- zUd^6Hh(QS`E=`3$p~p4O#W|9pG}6Ag>T|m4MYJ1c*d#4K@NBt+!+#uNV<)!?L(U;h zTbq69tj6CiAH9VV8LDAatkfzo>iJzM zd${!w2uSLeSP1YVhawiIMx7V~D8Y+m1dQLMH7(a$SKyjwIY_gkw7>f7E|l1QUC-@9 zHt*JWMlPxs+dzu>*N{VjTT^~+`3TwjYTXfknICu`3S_unHu+iZ)?VQ&_Nwcy`O~eo z?o7sw<*&xkW4dK^CNuNF5>N`~9JQ;*A?>_Oqk~X^4 zp7}*Lv%9|aaNqzd#TW1^amfQ;bfSKNj~@(R>$RURjyUvEB2~1P3g;o`V0uNmJP3!{ z6^)iXzAS(%3XKYe3tx?DYVA5}gGg_)8rUB5VbSeOQZ*HcgJcPlfXqSz>;U5+*719BrEe90Bb6j zuI=94FG&NpOY%tNxj%67!rr+vPa-ZDsjtj5iLPP|O<%DL@tsP^K{bIk!BBnBJgafM`aWNrDW=-@gRq zuY=yr25Hn&B}2W3CN4fgqg*ms(sJX29x<`z5k;28?KSZgiUWU4fhJBV4}M>zkglEp ztSkw}J1QVetUw!jAH*ApcX6)4?$j%2vO+iNF~r;(C(H{xdbi?hZU>D~JhAzpyLEde%3` zJab*>D+8o!M` z`D^0t68}98MR8ECm037L?95F5);O5wYd(3I;jqkUY={Be_+0CX*m{sV72CRN$ha8P z0WI*8`0oG;oaHDoifaax-N{I>1Y?pxi)OM&oT8HU>1%=kpMe_;A}~IDW&ZA!Zwde2 z3+JY75adsy5lkrYSHT<(yV~`Bl?min$thJ){bkiu;G6!oYMhk7Rt2HcKe}S)xfiGp z6{6hNHOj_0i?pD>g^6)}7HQH3nBsfhZ|!1LyPSFq)@X+lE&m}-*qu)rJA5X9X)25z zfCa3O2rVT{S6^PR`qdBnjq*4|AOzvwqsinu_lFXu&p@XX7=;88cBU6(F~Kf-BoaAjVd@T~rx9B+p>q&;mWGaTbId^AUfv7r3*1@R*H;|b9$>GlZ5xW|?qd&VT(p=m4 zJ&VEqN+`>m1=;eR({aTH)xst2EC$rw*L6J-(AgJ|&}uz!seq@aPof|b!EIZgKlWhHaG3f(dNPe6F%hDHq6qU{=jGM{=fYErPr!hONs%b8r1Q2yoUo`Oj8 zD{!Vb{;dQkD zQuGvbJtzIwS)x8~3QR_D;XxYWbQEQF&Ny)1sHc$3a3qK~(@XCKc4Ub#1Y(clE+rUb zy5UnTjZa+m9nbQmToepoVmzEqk7JeVl3B1IK4#TmQi9%Im0liocTYxPW)!hC$>wF* zf8Ubaz`l;pQKXgBObNBLAy?rTkHNs_w z)DZr&Xqog`ON}by^unC=Qrv*0ao*8tUZIRzSn*lqJtSmJy!XYu8|=!YZ+w()=<@7y zJ`&efrlUMV21|=W5P(C591xE9GQK=*Ej=Auyi5^+IC%-9Ltz2$v+RS$Da3`-E7YnZ zJUL&znF7OJ@SfH8yqG3}*|%H{ByaspRJiU0qS^7hJ|J|(LE{dX-Tk4AO?X{B(djwD zDYH3{+(NC*Df;8BaD0!YamdlCxn(;vb_z^}5sjGhra8N4P_< zIl*1bt*s&C^hgz&e3GGJ#-Fod_#qHmBQwv_-Bs09zpAdP&ME7)=D^;b98~VhypxRhZYQ=6rSfqw(aa@-oyC-uJ#CZq z1n;Lo&8l7PNZl~%5$(H2!rq&bF}3JH(ESj$wd-;KAJcmsaWFz|z*5@zH|}@cjJUgN zx4rIa0IW$jhP0?a`axs>LHku47Wi!$_=Ntj=jtoTZ$~vg$ElH0XM4{!G@N3JMnM<+ zQ_0|)C^JzhV?>`kGV0@6Q*Je*G4PuK1QlXao$NvU=A)wE$3>w`vXPOhSWb38-g-h# z;}b%r{qPXvz?yn(J=i421~!5_epf)s$alxb#D0B2NJZ(Txzb$GxjcR5P<%}A)F2(_ z2{v6TLY^@yB=H)NbVx)Yd8&beGx-xa)>bMv`#PJt?Z%QG;(ISf0*h+KHza6UPZ^bc zP-eJE`NK!chWfLQ6T{PEPY9OD40i7)+fTmRdKQ6fl8o-Uv71!V;A#L*6lt~9)}nF_ zXa+Wv+`locxl*88K_DDz_Cop{&@dqyWn5X*03yBOOAfRN?;#0;7M(qhNu~}aMTbD? z9z)Iiifbggo^cDLeGHjkv+(XaYFNmww3VX@k8P@B48=bhWrfDQEp+f?s$qEK)Hg+&U;WG z*`nz|ROkb7P(HjF#RQSo&Ix%FLyww~LLYo6jI)(3E(h17ag)zIg( zY5TRiHwv_iIfF=P{xLHno0seL=PE}4EyV=Xes-!?7Cdt`v;Wxw(EBxw@c?mtRP0Zx zsg|90E}!%s76=;fFH7Ct16X=OEiYRp1RCM~x*ZSwj4&=3I#ceI1ty_6rteKNin^-s zlbgdSp7(VTX&t!jWU-Rr`P>@jEuYlq zSI#bj4u@`EE-25Pj_Sfkq!?vB#tHzzOyS!m)InG%K$#u)KFc3kQR^Fk=LaE(D}?&2 zA1b-ewXie91J9G)YV?d!&6O*%<`DEW6u?ws#-gVM;1HkrImQ4O_d`&|6~DLm zceUUV@QgC&Q-|>?kNpQEU`*!aPX$6dLvO;|-M=y7;^I-=ZU%ticd}L;Z`|yyoe(Hq zHTn)70J+p%7$Gf{hbDU#~rWF@}*H8ho3pyvX~VqeV0MNcMn8)GSa zJtcBSMHmzcKPmpfLmy-}`e*=xAt3`qusRa*b{lEvkwr(>VLS_z1`Gw4MzE19)gLYI41%0UTeu%e!0~|=Em77 zM_7i8yoBg!bkxxJ{h1hU2%^vk#J&O|0iUByXzqprL&VNu&@3SXm&yN-an%n3iIa!V z*e#0C1VutV(e@2+Yh{r)NG~RPr>nOAQV%>n^lGk5EnUI^IK$T(gOLCcuPD7QQm(u& zx|tQ}n5)X1?7iHcAY``pG%FY?W);AF#jg)|`+?=$7^ci<&6Sz2Lydj}A+d;KqNyRA zemTvqT3Qv9t_S_TA2W{IvB;~8gOvrH!{}NNU$mhQXj;E%6B&bmBQ=B=9YlZ&YJ_%v zaP@+lg%;%{th0+om$H&Gj>gBo$%9IA?E`)i^faJs@SNPRdL)2Lj1x-S{}vyC&(3E4 zfLa_~E!d7wmp68hippF4$<#3ud}tMBpr=-#&GxCu%hZ>dqIgA$)@GLyQ9`;r^y5*{ zSP3b&hi2XKw}uKKW8r+Uo6XlyvUL*!G;f)xKHfRONi4G)3}r~`N#)bZO16)TY;T6E ziNkxL6igD-JAjdC_wdnlK{(e&=PNBahp})|3j+@phSUe(Nnu&1d#G*KmGts!C~YrX zkN&z-t;9emXmN->oIxN@Pw)(HmRohkbJIQ<*gmHW0IR{!5*VPt7UQtlh)7;+`NrLL z_qis`WcQy)E=E6SDojv0K>Ro0sH7n+dMO+{R_y z|6Fon=%;PH%927cMccZTP54*>1#RAl3m8vI?foWHvFx4pVt7!fU9R3HNn7IkS1tfW ziK(<7U8`9@SQNF`M3;Q;W4Kf%{rs(1`13^VQ$Bt%g9b77S*NFvkpsrt;ob z_Vwif1RRMD5oZTVq9WrF5romW7Cl5U1hN_5sEAt1ix)rKC_(rF#9tCDzBP=NNxm;o z_oOr;FrIYz<%l9!BQ5%Th+UMb&$%z9_m15z-p%*VU7PG=EXN{S20KV$qU`$QH_`II z3JO^6eAN{PoO1t%InJ=l2=DD~rubm6dbHhT+2ucv)@*_&T9h5Civ>1Sc7kTj&~(T!WA| zSa@?JK=BM>$O9 z>q)J}+0?C_P0bG=pTX{(>?p=}3_|f*Kj{VzF3jNRHKr$62Veo=(0$i9npTnPirXl= zn9s;b2(uxWE_O;B5jpmhTi@N?6+{dXY8YzqF%UK%LBd5En8b@ z_GNRW+)2Ds?{emvkUW&l%DO*?DHZVD{W*A>DWIRIgL4wLUoc=5$(^Tv$0ho~CmFbh zZ~~a*)lY6HFsw<)uMe|{^w|l{Dl^-tC%Dxa%ifP{Bj;fz(}e4ZSYc732ZFGRnXh=` z6QCnp+y=(Gz$(pp!ntU!_yu1s_}mKMr@u)!`vyuyLeftcA(B(Le#*c$l5dbn7ojac zL)Bb4{O!#wm_+YZ zad2a@mWuE$h|H+7l>i^} zKnfi65x(fGmK$m*I|x(KJzc~x(G-pYhY+l>J?3pH6JQ;wlY`mCoN{XP;H}ZC2IBTk15FwY5L07!IT_t1F%__SCuCw~3374Hjsj_WFKV z#md;CLEl-50aIzGffuq=QVOm@M#7~1Z~D?b!JKi0P}g%wG{3aB+&pc@3+U7cSlBP+FTOVfIz|FS1OH?QCU9FsGo zE9ECu&1ap_Bte-KXS`*xv%XI<6}Z_|G&vFaoQ8Lbaz?u&Tm-1ryhuiA*<|2}PIXpa zev;bHkR&DCNdzSZb|ln5y5kzGe(SVg+)e?L z^;@S=D-Gr!ITHRiIm(3c8Bw~OFt1IF@-lxc=@nKA!`%%+UXgzMOi3cOx>6^WQb@tD zx$F93TA(ta&Cft}aSxwJYx9*$HB72Ex z=Hbbklj+FdIQ1xa=PLCy&sz--!*TN(7Lvh3)|K5koP=okR>wimo{?(mDoGZegZDEf zeU0GC;tqj{{M9ujOa9~t%dhzvq`8Rkv1H1L^T}qO2GN|9wEU8L)jCu48L;_$+1}@w z%g?)l%^GXNDm^!7YxEPt~GvhGg$cn(AW^{%h)k~A%6!E z;!9<8LX@MZeSFeS4Z9WsRcHRZxxG^9J+q`Djcx@N10sh#dOnXRAqNF6F)Mj_yX{?k zlcSQJ`h|rbQcn6I?S~2GOjEJ#K1Iwqt41T{OhQSi3ZG0Dd*u?J2@TwzsNKBr{!$>Y zr2eU26ks+PzY!{h!|Ng*OeT)4!2C@oNjJM*`S?3~#Nu=#wtMsoj>XPvkGwU@&)9~W5r3XfLy=NN(bNXk#(pxF${mU^YPz*BD<6wR zUmMTQk}6hIFPo2M>dH0l6x#H)`IHapOxYM{k&7v82-V2>{FuzL6&c9!sxv=b)ChH& z^tV`}>dJP~?0C1AI1#vWM{FXqd+qRa&CN~5NDwrU#iXK~5H#_WT1@deDt(nN&sdgS z_1I}xqX)`$48OBS?BW5!5Tb|$2axbl?UU7X)n)U3PcXD-W5{ zZL_uU!;)JJWCXh2PfARww2ETuOSEciHXQxqJ=|Yu)@D7dDlMH~@fh0w`IguKHaa#@ zTg@;Zggj%H96$TUyH-`Bu-%iCNwi_pKU?_J`moht*9!VFT<7GCo9(~tXl&&dVT9Vs3FC!uL^G5feZQupI~h07 zFj=GGA`&|@>Xts57m<=FCREg}(_L5R(fn?NbwPtUv2!War$(j0tz4@}#QFWSPmi#f zk3C&?Hw*I9QASC}@%o~NGSt1)L`mhXR;W;%s`_Q`qnqY8hS^gI8W)e1Sbud|7hPB?U9X=%{Q$FuVJN_0oM zR5=RDQ9runr@_vvqN53`64wKCBhZ;SELKWT~sm{~mX_>Z(MMN8x!##TAyG=f8nl|)V47N6r z=03g2Pu!7~9z%}nYz3YwdADP)m*cU%56W>ikD;qJJXxm9vMwp>Op03G{i)ipRg>NQRM`4#%cjoJfy`t78nvzr zr$n@|=l7rQcM!8DCk`yy%oH3Ou8#VT$$@t8x)fhezof1WQSt6BhZd!#K+ngigm$K6 zgoXv)KGuiH{Mvq_vw=AQZtQ&VLVN$@%zNW%8YT*4LO!SXy2&(Ka#YC@u(p2F?fMdE z>-{{fHPbu&Anhe9y})wcOj%);sfX zmtEK1DaznzT~sFJUfm}h)|tU{3bD19XFhvKr*O82EMw&w>wS-H<-pG)%=vbx16Rrj zrGJ9MS_oo|;)(ljm57*@`M8haL&YsP_z07J`~44Uu~3-kZiwABXHT!eKswj^;pGjV z60HU|-iG;(mGIOxD=%<$tM2((Aa8+Hu6=Xm7~zSb&*-PJn(G>d6r<-go_;;rnprC6 z#;HmY%_39c#7`5mXj0Oq|MDOh*B zu;%XR(NNNq9P5K}bEWNtfnzlebaBIn-`K$O0L~2kBQ32woL;d&A_Pt0^p6lE9{bC3 zt|f2A^Fq<>4ikrE_xxU+r6<}jn_s)-i4#dNQwwDW9d3eKh|tpQa%Jn2*yP+Nn)6nP z1CBjbW;KK*KPU5dD3>~3Y=<2>Xx24ovrn7hnY7`-n!#!DiwL_C`AI9M z$ev%N*|5xp+De@^_UwMK$ug~dcp;n5@MGMbJ(j6gI;RsGdjcL2WU%@iFYR+L!l9mpf<2O4+4Yq@y%1^WpCt30G)z912rjBYxOMxAF zB{rQFSg|9N*H9$nP*^DRtdJ5~5z`SIYT9PWpgX`A~sNJvZA27zuGGIDskcm z|5_sg8JDXj7vF0Dj`GRhR(U6A&6P+CX3VV(hQ||9bMl~VH&>hHC7riMHSz}Lg%5>z2aZ6X zoZXVB&7I(@W$a0N_v+PG%S&q-wzjG6M@QV8oF9lf-&>s3R-w>ee{FSusNLfd1E(D} zuX#V`(^(|PF0NTJrYR|Qy{mEu{;;$s8O(dV!~GQTMEb*q79&&Le6&FIS$>s!T6fSU zZ-GrAhXS0c;j?sVRmMXn4cO^vvPUweWr9eTn4@=8$-~fh8vyopd_U|p9M9FFMv*#gwAM$8fDMm!Gq<0klL)b}Sy*Tz zvbp(@@VduX-n|rO0v-+K0{x+kH{;7+FmESdzD$Ts#JkHT>^dmy+2hHNH)B=vFxWaZ z)wwI_ieC;^^Ut!5QiGyN|9t_x88%_xLHJTro4WO;7gpti51h7a`o~?1UdbZk)r)<- z(E>^Jc(ShLssiNU1}Q7mz#|@V>|rxgL8?D%8cSd>AabFsHmB?xbCvK`h)!r39^Wbx z89(h*Hj6+q*qRS??3Y{gK6w%jm>-r{*REAtp|O+#H%7*l>{sfDOrP%fX{;F(r>*5& zIOu8-k>3Q-|HS+tzT(Z;2h6RZkBM#UyKpr)5+A}bxqG_gS6q^Lv}*RUoH_@m#w%T9 zoiyy~5Ia?Z4m!y;N6%`!51vMj{^eNbT0FZND;?&v<%%D_J5 z%D@=wat8roAuZWwJ3*5@IcfkGuXLpX*qK1bgf-ppCyxrws~es~*pl^40*N>(KJ5AS zK+JAJ+017vqSmUuP&DVU$~v@91=Kx{ifj7V&3PtQW=l6{kAll8Uyz4wI13t2CRtLB z9Pfbvut+EPq2^%?7T1&-$ zP1@e?a(RKd3q#G zN@-FDCVK8aHog^B*E5Z%QdBO9=u~jr{;XVbUUfJmffzxx zXCQ^=N`A()$DZ&L_f@t`0vO1XwPq=lV*8Zke2YK*5V>~M{Sbe*N0Z5PB4+Qbl)x_M z3*(%X+g6Lm{!+j7)#{&>WA5iw+et;I`=^fFZ>Li8wFeZnxYXL z=Urd!p$^a51sE%c2oWeEY&r?zYOZM?aeM1NbnBIe?}D2-?6CP^#(>87tl2F~s4#d+ zU>eghLf2j+D*Rwm!c1(VOMGcWFg`8G`LtGY!e^*3bF|B4?=~OacW(djTHKn}Ckj8BZ2lw3;c(KX2G3{TEXo-#l6 zobp~yBGupKpGbgwD&s@;z(j2TRDceusSgR8{@9T0i z>mB%+w~({_&cOOy?l|rw#Yw$@SYLD$e|ljqc@1xY>WZy_bZf8Am{nQ5eYmPaRFB1G znh4$aX&@ zFih=BNhu}7DSC+t7unojqJlo+O;nTgnkogU5C1WgGM>&&=wxR$5z{5E`Hf=fs>CE) z_6yl;XSta7_15)t{oVD53GY5ld-%f~X>t+aV#@f!7!q=`FIx`g=wCy{ z$la63G(ph$v*xF*pAv8_gKq1dAHt0eV`be5PFBf#MQfw)&&NI%GHvPQZ{EPWG>QHH zG>QLuDF|kF6hoq0z~&*obI&*poK$J{1!a}Y4piy79_pK1FB$ie$=W;hZYY_*0oHsG z@}zwCPe%X*+&QSeN>WFIccii5JkU`Dp?5nZEwLczGj}b_RZv4oj=_w9doo3{I%@}1 z*{e+D?pz=&Otya_tRTC*PMJ*z76S+v-tCllL<|Cjdiu3Nufw4c+7IL~(P{of&TjsP zZRH;*eizshNgZPU;yAE4a1RQbW3y}X4S7Kb7LZI7RiFOX3GisZPaNwqfP^RcPq+<1 zXvRj&(m><^^&h=|{ecafP*qgA;^R=jD8Kke-olXh6Sar_2Q7<%=l2K|v?^x9FoA)+ z($W&D{W}O5L=^2v4FTCLc3_-u5ZF4*C?OE|hekZ$H^cLEuK(!)-z7Kx%`N-Sxxqkz z72tarsNwu)payWv=@3NI(?B>A&qN2S!x+pcH1y7wK5*p5dUr{j8Ut__a6RzfK=FUG z?7vy|-@EKT=)C`{T}G`AZaW@n4gYh70<=~p;ay@xyOb(YN&@NXJz#6_y4T=k?*Gk7 zu`pI)3sOwHBu2gK#9s{xQmaK z2%Y)CJOv&efY8O>pnn8G;PS{ZM9MY0UJFN4bp}UNlTT7gc^%N_2oUBPKE)Ob?JpTY z!oUr5H(1J}CLM4hiudqwIuVAkubh9ZSmQO5C#noqYKne^0-6}L6*ikm8H8Nw;w9mD+w0c&GJn2 zzPZVpf!rg3*@We%-=UWb~Ai;^puu#)k?Y#szWPDE$#2#dRaFs5kHC zdX=@gZs1K&g5;rrkEo~1l0=b#7#|Bxt6(lsGVtT@q+1uqz*5vVjVGJm zvQPekcObi`8y_EAvOLCQ4Wwb(>p4SIL@mx-W zI3|IunJZLbczEYJETxkKmSXtm9i=?$7Z}IdTjR0HvFu)EOtm(7(wJWsglHwo5+TfF zy(}ZN^Wk00nyH5DHqEYV-m?^MtTK(&Oe85-L9X*W4XD$&f{sRrr3+sKaI)nfo4glp zlNZ?|8?S$M{JMtxwp-)XH|sQyq^Nyt38N0JY@Yx8l9WpTYw6x&wkrvfpLE+aeM%N{ zGS{P%Ql|}DC($Ln^%kVwh#xpUk)W1^8N_iVxR4}VBQh;QHkGqfy~~ddV_p=_V2m9_ zD3X%YL9p=r`j=AUd0KzMm^oThzw}q7>oBLxNHB=mc2TXODw6o~q5H|g|4euQl}%sB zlJsp9!|&KQClUP`>-s&q%_ne(VE0$Zh6M5Z`6GDwIIbP833Hn}`p7O`&-&~Yf;W*3 zodn1Nulj}(9?T%^HQv*4RJN~!wAt`MHg#{q3McELrD;09pw9CiXcL;0ERn)&V|7s6 zPvl8$zb8iumu8cN9oH2IqW&BTY5--F15p7xHd;^y;yr8HHu(!GCwe4c3HVO-5F9`2~{6w9F`O2~T^Nkj%Kgp7sU$lSzlH5vb@) z&a0#cE{WDwA0v4NG}W^w&Sx>LWY++c6XM-smqC9e&OiX2AZ>}iNBY2^I@Ogy7NqP^ z(^0`Fv77}_$&yCcew;x_B%ff<_~w_omhE%V&7X{Ik!cU(h$KHaQhg78UYY6vZi!(v zoz+lJ4?>2pxMWA+>cXjn&AP-wKWz#p3}BXXncdtqqOH6^4=DuoEZ2-!iDugA{z+P5 zn1Sa2)95S@(qaPM zUzDz(Nx+YlDPikE_JRSZIph0r1|!kS2Zlw7ztXE?i5tOvQ}dcbztFXk1pI50UliIT zBLcH&IPv{HfFyYhkWlyUstr|0gOH5YZKrnC=o*3js;v|Cl zj~Bz6G}>7F7)|#bmB6Pi5NAe8UWoY!9iEq0J5$4`~n}B1V-846iEycF314Hw#^25>*J?O z9SVM`r_C+#<_ZQQ1IQ|QDfL%6FjI47vJ;&J$l4MH)2JWyC;Ert1rMpSNdQs#D;Gc; zR8`nK?WZNZZ!Dbtnt>f0jq?FJIP>U^AYjwq3KIP2@fR#2PggkC=k zeeoUaUgGzqU?G?F)W9F4;GqDEfqVukXR;|B|2_saCrI$&e?=DpCiFNzH(7wDbuFLl zF$j60q7VCZ#6X!)2wXlC1RhE^#%BQ!0p|*pC)D^qr__ObZD5Oz-&FU3G?jm4T>Y2H zZvpIK_N6_14Q5149QGDu;K%`Hk&yo_aM&;#v#B7EMU4fV(#nl8qn3XdQScB^=p*nD zGk7RVsu_*=f*qwnZ{W~~7fu|t>zwO?Co&2Z`r*x^yE`=fBCK{F^ zs?KUQh!!}VxAQE_l!NSqR-{4hzPKxJ0KJN?fjaKL6(EqHZ0&#>;31F_CCWg1>km5~ zazAf&mBLZb1*X;(B_L@WjsgFV$WIFPpT*@Cz7z(!=V0@y0_nLf^LM3z1bjk=FC9o} z6UcR5_h1Ls=HjOFlBjD-MHmBY6i2=7ot%j=B~XVR4r{ zoN>QKG;}1XiC&?W&D^m`s}u4E$E4yb9CUBUUGJ=?PkSnhN@Vo*9gq4&yny8a$Am!o z_6cbJArPGJpnSkEgdhtI=0~HpQOT`Fn118SECu@fD!XOF3|oCt8zAXqqqKdkMk9Fb zT&BZh)%Ria9t8A3*mXCb!)yi6bNN1BTe--iAZ{#l!eCJF%WYiCnRSzv{Q94Y2vX0-!R3iD zE0qdWimB0^nm2t2c7w#(3boUf6SnWqC(NYjuO)nOk#OZH7wHHvrIRqCg^BVDr`ILd zE&rIwf^T;v1ko#yqHu{my^-@yo;>65kap2)r4ogOoCTNa2?vckk`;B#&6R6J^xi1o zU_f}+Th9u4<9q$rr%t=KUc7uECGe|q)Z~K~*eh=^x_ahY8py7g3pV z_dBDT3dQ{BQ;=V6kQu8t%j}x{O-1xYii^$qVJfkjVu%*2L4)+z*c}$Vl7!P1R(GiY zy2JMJSu%3vyc!pAuUC>CrrMA2!!7WyT_io5q6Nk$^70O4rwQgHB#@egiru?o7r?L& zY4(HSJ;28TEl7ODv#}M+EowK3$Bfa`WdpqEC7-DJzY_n2eS0HE|A%GOONYmvQFFsL zKpIa}%furR0RwQw(MEau{0|qtt&jbu7)+#tQ6i2}pa^+UB{Yzc^9=75f=XKWthz1f z9X_y@>a>1M8;d{9{BKXM@tipzzr=j+KqUPuqV|sZktdzCUyH*`e0(6PNwly;t5{{u?NF?HnUSYX)v8w=#btDflc~ zTboDct<2frhqk+ryJraHFVtq4pnr&^&#f+!wIUi4@QyS5xEEjFP=)fJbaD=f(6mZE zq4Y=90zmGg6Hfi6c9GDeZvr~#7A>duuF~9q(+sdt-m!Uhcq4_J93M2 zf{>@w*qmlJBSpP{!sNw=o^!|$3iSX&W~jDrDae-!iUcQzQp%0RUwJ=^JtJ zu4a&2t)HFX_6f68SDcyOWRv$o0aq!A3bA!NGqJB@kh0%2u zQPXgAD!0vj9Cq*ggo(D-4+G^>quvE3c`KqZBa~e_G1=>DyzZVBl}N2JKM39Y&r>_0 z?>j!wwLa|<3tM9q)tT9zaf~Of-LUq4BW?KCTEQEM@n0N+`gyOxZZ+wyT6+LP!ARu5 zTQlSSGamJaB4t&*Btf?_Wjrg7ygN_W`tG&`O>BvjhYDAG`A#*{5bM?`J&OKJm<)Mds=zc8Uek{bP-^ zDn{Ef=hCOWHVK+3mi603U94FT;jd^Oy#w%yNXI!6pdS>uTosKrTx=fCsFAh0x9{x5 zlsbNs`cn*VcoOyR0|u` z;q>>B(VA!5i&paQw1MCl0rdB3PPyokQhpt@M)Ze3!UDf!tf&Y|PkvS(fl}-Jz4+Pd zen>o*RM62yck|40@z6oLgR zJcK!S9049h5j~q4{Pd!@;F9BD@_0ayMFJECA=zx;yN+DGvA-k`pnbeILUn&juwOO+ z@-hYKzpnZ$x3bj8UDXF+0Xkz72iE?8z>cd zWDFTP&TDk==HG+^P-^kuSXOEftMv% z%1?|A;l}{3(>Krw;d0Aw0NU1>DdPS0OYsi`fN-p79lx;93{hUdS4F1F@BD6n$Y;?e zn)uP*zingy05?{A0JN|Id*~_Op9KTrTY7~m8fiT0@(y$RUioX-#fn+jJ}@&6Zvb9D}M4|(4qjAC^`&Jl-GBS zEBViNESHVMt}Ol0e1J-^U@1?~<+#9vjFVu0=@Z|5FvK+1r^{40)Lb87gTKT)GO-T; z!C>Ib7=0qV6m;>|z<<{|xIzse(;vZ{1syIb6{yVtMg;i(`k!3}*+fBQkJg>P20>|F z(Aey<5aBiW?oJnoDqqU_OO&irUCBVDkYB!4AIPE$HhnkvZ;)NAh)&s74x)hg2 z4=}_A$>7z$qz96X0p=s0eP2ML@ecUTkaH0G^1BaBKulOYue=rg`?oKH!BQCBa$iLQ zq$Rof7pGlS0QW?mx&JlD74#VK52slT1BGM_eD9Y}^6m}$?;EON=K!`*&vF0?)(D$} zK42HW|5Fz8*Si1>Y#>n2r+EekhN)zh7A@ze(WJ^^{b8TXf&BhQWze_XM>u{oYDm7~ zIW}|t+BDV6g7a(NzjO|&=I6TMkM#O1zHH}i{9GaLu;D6DmO~AJ0`WBZy#)Ouln*eZ z$A!vPI%oRNjwv4_I(r71&YcP(w-?kQXsWNp`LTc5SG~M0tq#cseL}VK(MxVIniP6{ zeVL_&r6}cVNrj;Ocf0ms3k!`to1hbxMd$rrBDS#w+$Vt&Xh%@ zEBBPTgXpM@uA&us@v65ULGckIv2QQrv%haCDF@c``Goz&YK$n#zv=bt|v)%q@;_oEjTC#DpM_{?FdOK-s>H55S*X@Md8ZWd!gVT{ z)bjwd!*S#NeL6H5*?q8{F^r$s45nq-M?<4|%fCj2>}x@DlWT5cYYz_^Uln@A!`;{2 z9#MeSDc?LMqRHfB_&ro#iI;-JG&_dGMf0A_J2}XK(CF+u_ISf$Ys1oIikG}WNM9Ur z@NS?#-Z#23r*uD$o}S4Iw>^_OraFka?{%mVRtqfcCSj14R?b{k6!(60p2pO8zSRA% zxvf_^G(DemgE#|K{M>`xnUlwv`&KupL8$r)#w7LIxT)Oniqk;VF!peAmV9zPYr3FY zwO3p9ueKF>(V4HEG>g438r0;I7bgQtyMm$Gt)zPpk*eN}Ug(83TNKTKkBCt<-$Hrl z5MFc!R#FN#uKc}wK3d5xQ+LWA1p^M>jcQyiQ5ilF^NCe0G5b=SrD$qH$$M69f$`Y= zv!Rz^y{dp_NKJ4;IrXfNU&2a#;qrosJbk3TVOxLd<~W~lKD+HH7Hf+aRSu|sbirmj z+HZI~6~Z7-U$s}I@P6bnm+V4M%!>I1Yw~%?~(w8y_9=GORI@?7Yb0 zP8rKP+0VKCJh}5c>Te~Vu`+fG*yk!<2FGs#%XXEhw$fax0SjvBKhK7ys!iq2%YWK3PO?mmEoCjXZgze6J~;!WXE79K zKMenjhfBkuJ}WZl;qY~UqrNeG?#o}*sn>2-577t<#BoSiywjPc$Vj&hyWf}hwMbu} z`#nDZ^H^klxfL_nxeG)+Wn~9=5muRzGxC|A!#aMH*}jmSm~N`QZ-t0+rRIEsUU0sp zGhCqCT4q~4%U}Pe=L)K$%SC^R^cWF3uT%G>1TmK)LS3*nbehkzGiTLO@RH)ThWqeX z$tA~ee1s%lZGQtbDi?V(KzuoY>=pr{#UspPAcrAL*>#PKbYQQ-dnocI79j}F!o@qq zd^F2gV_7tuW+VLdkD>QCK)9<0E9%pQz4@SsPFS!?vc5LB=i-^)Ma+K_tcTTa#OIWC zm_zSZ{;FK~1m698m<&x}6R`%vL^eDGIY(9$A`$WnbV*?}YIe%)iVeQ!PKu}V+2&7@oSN9$#8b9>5Bw|>(iq(z0BAYQ1kVaA?q zWj^?)zuF<__)L9Undd5Hoy1_li-I8Nd0p8B)Y*z}SsU7E!n1PUO1AuFs@uc8WsbQa zwc{2&K!GvI)OYgQA1d~F)?e&Kb#Ilwo)lc`B~p$8rKhMd7;F5%2#Z2ul(LGBz$>ei z0j(bH(AZ8&>S`)P8R#8*aBmIgby~c7h1!e+oW_&+(4yR2(-zmq@rf|Yvo=9uVn^nF_`y*Z|1#u^$W zx&6I2lpL)oNJ#8Z9UYE!u#fzG`7mx6Pg-UAlM6Pw4p`32zICprfUS4G$k4z#xH#Qi zxv_?gIss#{Mqvv-{5VB8-tLaYs8cEy8;2n9Atq_S8T0~;+;=jUxc7rMKyy-g@~dbb zz|u1nQW#H8!<2qO?gHS5`nv-ZK!`xJ2(=4PVRZe1EP6Sh-1JJc8Y&n7;QbB%OEBJu z>f+X1Y4|M6n;OChzTwoMi~It@l{%V{7ivVX5%J67V{(aC zzBs5ih$lTD3__BKBW`1910Wg>t$0yMz=ELkoo79?g zypA>lh~_1;?ilo+QQ)(&aHI(XGwM+qRCT|d9TqM+2MWnwvAeE>B4wBwIK8^a*h-(1 z&jvaSyq_nUo4dUTUlY#7eX-#h|Mc=C!SF;OVv4Fk8e?JT`pyls20$>-ODBxy$WJ^D zeEy~$CfnK+f|Pw9lYx3P0Az;=H@DDmV^snixQ^aT!VRkJ(YTfUz%f25!720&YnntO z58xhX<y6SJZy+sV2hF&WD`}!@A6AI@Q*)t0`MvjNuH74nCMTzXFN3? zS`r-A{};@D+QKh+>7ICkgQ$IQ$NYmv7ci%pcS01PPYTK}{olz{tcsiHf8_%FH-rCw z$l(8jOdXyhjbIkYdk`#8!c$x%Idz?}^joPYLmk8~E?AKS!_{N?Klh1N2T2>bB z@(F)fZ9I$Td)^=aMZMr>SxF2TwoA{K=VhjbXXcsjQ`1i?&c$$T-Lu&`c$jK+cg+kx zzDr&i-fwh?UK-aKaeFG7P0(^MzQ~)$5;K&b(aFf(oiSSfm1fQJ`23cGTo$}Ls3*qo zgq4~1FijGKP}>?#BXaMQS@av$s??c-N7+!)Brat;fM)Vi1F zdvIkpKKAmrX|h{rtl@XYSeQo=V4JufaTix8y-GCPqSsQ})@I3>lpTM%vS_(?XqLTO zoi}86EV5jEjAkNHkN*2*v#W}2gTZ)}Ok07@64OuF9nkQ{jU+FeU(ccNL=RWQCAHcI ziN|I8J)LW%$FpR)w@&la=jhAP*?|Z%S&tYSso%`On+DC{T&`iSl^KiQhsPc1`X0+G z8b0enJjKrn+B5V&(}@fZ;7J0q zY@f89G`!cjBQ>2}f9UW%T@VqfcZ{-#(9L8>J^J8I zDA(fLLDF3`vbpeVt2!GS*}5;_}II5p4!0yZ(LTLdsYtS>(WT%7)PrK!&P&uFV-`?>!nMisjFH--R3cO(?e0;pp&54{kF4yiNVGil}CpOrfKu zF>ZZjx7F`e)9pa3&R>BsS!y%nnStyfQ# zVbNb`TFngJFApzT^dz1=e${)XJXLYl-9=XXyX(lI@bT%j(Y*7t#9@!~3fjA^GvGem z!r|}E;57f+)`v$p#*?0iN}fE&7KX)~{*&o@?hkd~iH9b&Ec4GTGpx5K3>LfgtJmud zJ?i&foz!y0I%FK?J1BRDBE>{93w*e~K#MG_HMfPPKL~(EQ}u15787D#I#p>;&4gkE z?j(O4@+V$9+Nwx)nG5sLxTll7;G&oh;Cia(?JX zxAtu-VZT}{clU@-2nT1+bGvPTeuFJ(J2y6Gew?-J{7j~7!=r*_a6*i+wOikN2G2Wf zZmmYXEjixf6IXo?X6Kv*aXZ`5rcliK(qKxWO;4{5yAMq(`#qzBZ!++}25G5_9!a_w z>C~;=9Z$D;A38V~8A$8*2(H{Vn=g(Rk60ZCU9Z!uYq&R07}<{}Ws=BZfvB+Wid5M& z`6uiVd##G(8q|t|69jurw?y79V$$JG;=IKt@c(UtEuD@1N9oAB0yTZC=e|nIAS?^=t=newM8xZI|UF%TNTp=WCHL zk==PuR0Y-H82%)q#kC&`W?VZz&L`;L*JF5YP-A;C{lg_p*hSMpg{IX~lYKBxRiURdT14+2-iF-Px057(Y-jnzqHO%gVy3>VEN{wF?|f}Schq7^tMo&9 z(2bBAA^tcY{L}NF%E^AeA5UL$D%P-Q+C<+a4rPiGikdl$Q=xa*5Loo??d}$6I2P*t z*mU~U88i1QJ#Db#j@;aX@cu*pdEY7>kJrdZoZfelZd@5B?RR!?7*?T0lHa4-?qny{ zi(F%qaHYJ4@EN&#@01K*hC(xM=XlpA*yEAdnD_BiHHX};#8cs6nZBLVEKZvv8e>e< zh0xU9SQ)Q4r=4Z>y*%V+^n9}Q^j)TRvv0)C{iGbKc+$jQ{y*CL>YzBHtb?QZCs`~tt)}xIjs;~;Y1;4yt*=i> z`G!V1D0$b(b=X)c`UH`}oz>IocPUaY7?u-6-;FatktuSUKFzjhfn?K@9nywVe;R)@ zJEuJ<-aL5DzGNv+zi+r;Js;3_lg;&P?Y%I)f6unZ-`v#_;4?YB?gZ)p zYtPG4ErmGSI`9vPA#9wn#0=PK?9ln9-oPv!WfvA-y^jNk>jxK^lQ zGM9-*7oJCTn?zIbHQ8>P4T)U}q5n#a=50~0`s9W8M?K2p6sbG_j=r=8BLgcPTdXi> zdeq4EaCV6r%HMuxELv;i?>BfU(ZHb3F%mQwHSme~Yx|e=+Y9<~!&68|#_m9dKHDdl zg>`z|)p>4W*lF#aQCnCplOUrZBUUbZO1t@aRO7wCP|lTT{cJX#-JgiUK__$+--)_2 z4MgXyBl@qFOTT8F0a4c|vuvlfSqz`Olf~{6OhlEh6iK&UzP-S8Ou|oe;^@NjcP911 zJnCpytNIa z`<#eb&!0ucnredv1&sHdx&uEtZappcJFQ(`InnUR;GgCqfSx}ev{s7+HX4QUD2qNr zfI-i8wWx4#az`1vcp|tcyq++;0>M{gz4Zdx%$^-GYSebq3d4O{m(B$?wPu-jc4rhk zsKdzj-myx2OC6U56mC>2>1mY5B+=aUjY)Z=CwdN-Z~VwoLo5l0W{cXE3(h_(aTi1C zvowMnzbJw%J{kOvLT?m!7BP3313u)BBw;FLS8bw4oXS1Y>G;sJe>+;an>dmw^zKbJ zx9rw97~e?C_>!A*sA~rM*s&;|OFW{7Rh(c7g`R)*o&L8#p zI!t~whyaR@mZTv=D`A{VL5&!QY56xYW5hvOZ~*LHV{#1Jx70Tn=BxCxikm`=V-?mk;>V*IU zonpt2fer+zq5-m{_vVr0>xp6sm3Qx+JxU=6#L)4ww~9hv2VLRC!(wEB{C#nQ#o3vVq=4G`%-fy4eapXy+c9TogF;Q zQ+^?hD%bL3Q|>IC)_kQQrTtr(VTa@%5@m?C^VxPubh}ZKAxX>On9K{J4@`5WvNnqz z5c^jBexw1+l&e!i8^g7;??Rh$x7ej3y{8iWy;39Bms;!lOVZv;uEo$8&W@8ccj2_e z>jnGIzYL5!+kerXn=E<3rx+`>=b=P>6~c)7NjK zI%1Au6hp3=akhdKgJlc9E*z$2ZW`UEh{Z}E3uDKZ<@yirCpSd+qCiM%*1W(~W~e3MCdI%HHzvc9#U$a+Bt!4@FlWu|;r z(;D_RSw%V<+SO!crchU+Cju8KS{LoV+Go~^Dm+hs-iF>q%aDi}GTa^44)|@9o51azL z#B3de_jg++nJKnO;S4VBj__TDbE(X<%GeOE(IG&}o~)V~9kictZHe>No(Cc_-fVky=#$dhtd%5b~t{o;Q>9R73EWOS5JpbmF= z#8AI7|F_MuL9q)*KxQ`#O&qoU*K}z(@*e3_sKe-R!c~mnkDTyLg{6!pA#v$vj{9Bx zWNaHJJBz5!Ka3wqw1Oh0<)X?-gMG#PQp6_hmv6sgVimTQN)9tOKWkCv@K-w% z`BOzAjl)mo^8RP}7ejl^e3JUFw3wR%-(ZOWSCI`{YnLSMDD-R)Q?Ur*$BO11x=PYD z#35(W-(H|y%S}Y~7mDSRuF2e-Jpq~^2@Ckpj!8eQa- zLFJEPnylybZN%);GH67MINn@CVWtB}&Un27CMn%@uPRo2l|v2KDyT5}LZ7s}VOmYA z&|01nfS@I)ikv)iK83RrbtD+!&paVkhW&b3-Ep*^blRBL@{X8G&S>fvN;dy*ftbtf zTbpQybNj`5h2v7iiHme?fdl^yK?IMmMZs-OPqm@z@=l`5BG+zb&_ao|N2vOjPIVNA z&!|CN+nwC%zTtUsp+rQ~Im+GfqRKb2yW-bxqAYW2uqpH`^SUHS_#Km$o{O>PxRKI3 z9@ndS9$G^ckV~caz6O`x?4-F~vk)ozIo#$m49p|auKQ0S*GkmW#>$h-XGG%^PK7J` zb8;EK++UL@vP7wf8rizd7F+zbnLDj5rQa@3>*{WkkI!>kpu8{VeECyno)!{tH*Min zG?zknAmj~``8e<)FP?0XlLWSGPtU`DFMI>BrWd26vKYk6{m8!l8TXTE0I9|A82`0p zG5q z4jUysVHpy8o}DPVtde2i%LTJbH=`#^l3SOzNR#QoVpnUbNLN2H%T%@4JQDBOfr+l_Z=sxU4%^Q5Sgv&? z+grYCxqFri)9GovYSVS&4=Nl44~d?1rzlRt_}@&X@`l?itLIX_1$D#j7|k6u_BWPG z+T)SkOFp-%_`441RVVabua)ey3tky`%?YEU4<_`))C`CaVtY!1+zsh@uAR zTzd4)K}8&|q{!wp&X2W%TK}ylJA95|BcuuaE~79^^(najsmPF!P1zyFQj2S7vL27L zj6GQTg6Jpun*Sgf9`$46^&V{bpJ}9Bb1WTqr^!OM(OWsRcV9Gs*c>4(DdJsN+S=M(#^_6t~XK2=^uQ8{Yk+3MPn$zhryVZn;y zimgt=$#jV&rw{V36)Ia33CnD?*8Sx9%Mp_COkO5E$j@y(SM-X>?a@x#!uLOVt)JJV zzP@^~;IyXmbsafhL&TOJjp1gl@b$J=2W3vJk|o-JjKF5*0n1^p%)#M|LY(Y&;yz+f zE)H&3wa(6PWTjk1Z+QsJ7gN-;Ya(b82Zbsg>mZh}{jv*$ZmnbtqqYTGItEwVvm zW`s0Wo03I8X8QmhmxVafNwWytzG7~>5Fy;{<2bfD2kXf?AqaAKhn*T6FLbiy<_5{~ zImlY=$0%Dz>rseCCMO#Aa+9dhuJE=WBvNO1vaF}=!}6 zB9}>>=dY?XXv5NO;CB&|h$+8L1AR$P+_7^l{@u0(|NG}|+WCKO)(8^=j_KySX8Nug zz8dH;eF-@B(@!_{VS}P_42Q8VxFzh?mZv;!St(GuVeh_(?y>^g3MPY>`Epv{*V0a3 zrKIen<{I8-PZ2}eHQk(v4URpm=Z_QAMehoqg!PW^$uYO?=dQ0HKBKkV{FZf4jFY5EeDfmgAe8uB3!QT9e2@8IvQk!Lj892X^RJC zi3#80P67(G;p(puP(T-tgST=<*UR45l8VYQWIAa^1T?o+_Ve4bgEE z2<@T*5YQAm?1-+vx9;b3r*x|@%RVA^E0G4Z%x1B8>oy)2IrlBBbMp66wT0(0jog{? zW}%avf5PX~Fzz%yFu_v9FPke=a*a55@{ne8ys`)|2+|gr$AFP@$x<$^DZzdVYW4s;p zTw4U*Tt)V+5eT~zy|2CsozMtH1o`<4X%OcWK4XHkD1%Tdk9?Wn8~CoVPlH}?o^ypS zT;H$?-A8I11&jIolv50>*&-qaMXr0KRSxC*FTLJzzqA_?`jCEDS7{sTj1}(sl{u+s zb7{K$1Zzt=FWlpR#w6PZT$GNSa^?1850eLUX?Z-#1M)4e>*FVQ5w8P#1>l%?z#hv~ zK^u5^U3nVhKIQN*XC>92*FI+lv*~mgqe5goR_7Fsf)0|?RDfN4>rEKB_e!Z z0iS)Zwsn%R4s%wPbv`LjxUJ79KBoGt#$Mq7O+-^fDE4|b?R$p)%c_Jv_4sf#Ttqi% z*WzN$MyAt(1Snh}rWG<96|KF%0&!&J4ncsBxb)zj?*nB>iPWx_wW)tqJ!Af|j>If* zimZR5&Y@SJ>k8zLtAj*3Q{kcNLzkB2KJxm)hQM~-iVGW=0?(tHOUQ+%hdvS`zmUFC zh_mIypXm{6xQ)9{p4Z4FO9f-|6ZNi1e4crwF>K7|MER%tmWXhKf8|4mgFfGbncsyA zQ>N-Dd)uG$_i2Zq6g=tACvbuEVI5M4$$S`Tmyubis}xS;TDKuKa8JXSWP$YYI(pc6 zrYySnbHtFQ+M?kD8SkFYL8a=+cf&sdmfhpK$c`-6Y|Y@D1lCWc7IMFee)L}tDX3oW z-tV+USZ4dJYlK;SId-BrA7sT6+aF^lsJmnX(+oI-;l&`#)4_lunK#B#>*#){>E@?1}wPU zg$}dVuSN3RLO4Fz$Rz~fq5%r0C71G&Gp&ppL)2OPChfhLsl>jl{5Zz%#qD37Z(MPI zVoP}EGI|p^i{ULdjM+Ui#=&%0s5I#>6&Q|lxb2p_=FfSpH*nRnTx$}}4%Wi2WhF-<*m_Y{9k&h=_U*gmQi$04}avm?ztk4dM)Y9|A55eMrXO`=RO16oZk<_H(L}y z;YV21-9}gPubEPab;DH!)sKhkz_Ibnp81dP!-e*0J0%PS_S^57u}I5MpGW<-UV!v~ zV|3M*a6NR-&h__=N)0@*t-48Ny*%T4o0X^O9 z;L!_WZgWj_7#MJ$aZ04g(iO37S7-N9Al7k8mR_yV`4iSYhs(%qJB7x4d$@RunaaCY zfB`Gk(l6W6;?OR@>~Ml8hBOUjix@QD9>Pg}96OQ0K+)ALx3@ie4LasM2jYsDg93+< z9TZ+l&AgneLTg+ET1jYH1%|`<3M!EU-m9ZHNKeIQMs-KHVlnT3k{|^AOSVG9WwUX& zNOE=Ew4|rRwAo(PIV$hG*I}c5damp>*ov|Ek@3xP)e5jfY-RE8Q07^jmW!%RAD4gH zg-2D|ci8}=epfehdFZ!Ey6X+nBE*TgYU4%hMX0l7NthnjtJ4A1ljGc`=V*vT(Nmpz z;y(nVQL;tWKL*3VVrGfBG{2W*A&XCGOBI&q#0lYy(Px7Xc>qv>EEk$fc*LU_w4E#Z znkuVtu1D38_U^D5qw1FH$@lQ8fy8&&v% zXql<+oL`u!2m!(QxFskSab?rTyBfMq(f33vX3yzL&3)obWd`{M7c>)_JGQETg}>$2qyOpBH0opnX>8QqFqp=8PT;(`6GQHO5fUCYT*rFdpo z-1w*o1BJ!$@U~vH<}GOGC<6db;^w*{yUHty- zq%BI#PE@I3VxoeD{APrtO;#XRpXY1P3Jw(8_z7-U%|?OCbn;{8*586A&nlvN!IZBE zp{0rhKfn6=oK=kXwKR7A_6=c|oE2&9)Fp#OFUO1C`)gtmjbm(P47iNGOmyVy_`Q(n z+xVqd;3tP%4mvgIiA?Jm?ujovSnG5pg3>_%3U`#p!C>?p-RmyQNB?nm6&U6)MB)|# z)J3JT3+EeN@`htA!oIf&#-VR!&lSeSSGOFjl&61x{v=FQ{`a^-WS1I9InvmK@o(H6 zg9cPzTEP^8Q)tDH6PmYTw5&%OoX7^w7O{ykHHZaggUi>Ck}#*&mQJ9oJZ-cl>a@OZ zT8-)hk0|9-reLUPwq*AGE_SoNMjsl0g@`><`{yeLeW|yOt?zMmk z*LKS^-DA~TK}!k_PLCaEEVZ2&IkqI0U;9(e0)c0D%MI8Ic=1qAOcPsoBI=SDGfc?_ z390n!VA6FUn$SiyPe3T+n%#UI|CAU{U2_%_iDaAU5P)(x89t6hDQ_E z{7JiKI}0`-nRwDGg!i@?Fo05U4Z0|!0n1@Rsr)VKsCB1Pr@HGO5h?tr-piQu6~W0G ze0V$ky5u6f9i!fYD5`MF3Elh5x{Rn!C>@;RwHUg4<8Wb?;JNIlo#bReA+Fq@)cO|b zcjmMk_PHS0F@V~<7};w%)EXz9*(3AJn)>3L<|(dIhHka?RCYqWpFZ4Hn9TI1F2p%n zX1`ea2$Ls&eY$x8j0WYUlgzNqv;2+B?M;t76}TuZbQ2nC0iW zlb{pN_xW5<{r3&;2SCc9mg*v-qq_5GFc}oDm$0d!mu3>V zw~sOyR$rXPHvNoK6O>kO`dEcKalNy0XbKeg;eLr_}N(7*mee7WvXq$cxUnFq~aMv_M_W1fwDlIEqm>`)p0u%Mz z1kg!mbuYySpKfGN+H>%?rQArYIhKV6qT?GXUf7zOpW)2PyP zKHcR2P=qq=sPuImwgN6}TuYoJtNy2hMO^>@6E(dvRbA7>8q;s;RHEC+{nFWj= z*U-7h8KqlZo;_MKPT|_|94DkUMgLrE{0)dxWXo*l?u*y1YHfPAoQ%`kMRIzX4BxV~ z%3%{uj(7*+QT6eFQBzDFCDYG|Zd2eL_c^vxESuTnCenVBMzW+)ApT0;ALf-joV(>P%vbi&%6wSlC%`*vo<_naE>QKLcOWGkphtGD`Fn!gIV`;^_nd{OS`NN`m z)w32})y`^BHYWM;EJ%PA|xbh`>?A|x!M4}TSf z{VtCRojJrA?L=lbu&F-tn)f?uxf| zm&GIn&=#0Wxz}aDK=)v08%cqv^E?d1f1k8_Pz=W%0TdY;1yzH+w^4oyRX$ z1@Q-MXxx9U+%#27?;72O5}!;v)+GCPU)Lx2isDR}rOE}zwnY!@j4k=6b82?^u690) z{CRzI^Hfni)-2ZBAxSyZjt*<^P!g?Ot~F9nxt6qcs_g}`>wT9E;Yvc|)5v-1R7-ME zJ?WYr$m)VFeNcuD;pn*5{;ITSJsGUk0PSb`7K)#kB2C;D&1N9jkLr{8@90`vPvW01 zsPSmU_+R~Ts!99iBWD_HD5UF4n4)?&dqN^C!UoZ8d`Yjp;uN?*w)PIp7>-+banra! z59vF=$~SNsuX@kuLy@s!5~898O(EW11jTxQgvpko`gfhSJD;y<1joGIE-jdwbUO#O z$l?@WG1OrRnisl#E8VR3A!n-kYAQWE4KCVgD1=X;XuKHmG0{N30Gwk08>*xR^6 z=b*(*w3LEC&lLIxp_l0brU{9HzI7W)Vw%KHa9p{pp&OzB^K^)+Ybl%MOO#!31$v#&9%h6p_i)Ob8JHZLg{uzgQoS z`}YMHC_Jg`sB^N|f9WYh`d&`$C9n;`Bk=%J6e50z;bPWi)|rpH(7y; zLAe=)u-2lia#~)Cj;WcC7U!6$i}G#$Y}EA*W_A2%rE~dS>QW&MTNH1H_Dg71C0BO> z6Q0zU3(_V(`24`GO_-11))Z-x^|esB&jFl$3)=n(hhso=m(49q`bahiv*Yfzw7+8$ z9euyg1w>KSpRy1$X`ttJ(r8_U04_EX}|8&STKt~DB%eXW9 zszsQ9;{TZK2$`^&bp0y)NpIIG$rIj=fs@T!7NJ7rPxu^(kit>CXjWa1U8=BF)I~Ax zcT&qXZnl;H9B^8h^9#8kpP_cPa)rD)-)0K%?W(xFFz%#Mj^Y&1JuV>w+ATO{^j+41iSGqrvetNTD-_6T z!E3xD2(EO|wZ~ROrK2NZAMZ0&aG9WS{3Htccp>3FUx?Mh!NjNa-KECqwwMKvA%lpy z;Ye04cwdb;oq((y!L7c7U2+DYZN}23Pm?}f7U!1_H3B}#yWPgF66^9IqxhMM#DWw=j=+2C*PmBtB#%5qziHv;!8C zmsH0$Jp@QkkVlw5`85B+tu``nvEYb^jzcDvvHmk-E(%VP z4nv(QSHnR7++nUbon^FD`?d(2vrB7rb*Bd6zs0Hl_!q>qK%P(q!6((OZjd-g z(vp`h>!O<0g6#{PW+jkA8%ykcrCasD3BUSNN)>T|&I^)bYBosaN6SHnY$s`gP&L5@ zqpv}pzcN0%7#ZF&0f;T`WUGBR!*)4Cy$Bo9ng^ncHI?jp|!>9fPcHN zh!fT!Um6h;bq#iGT9|qt;zaF3%%;hx5@$;V;SX1}#-WUf}CbVYrrGmx`sT1mCm1X@JmK_{O9E9{6p7Gkavjc3!@O# zA$#oN28--=_}6~X*4*}EkEQ#fsepQUV)NOG=rq*TSkGP0$AM$Ko(P<(^EZb@&KVor zrYhp-o?mwNr2^RbI;bJtr(LrO7)9sGHH1D6&34`7`RaTJvc^qsa&rwiKlMEwa<_}t zzn0m%$YJn#O;!VrJ>c0)@P4xMH?WDxNYZ0J@AUoeKbNvy@u4Yvz1wDl5u)9t z7iV^fDw9F-y6lH!^~9$d;{@y5EzB$h?G3%K|plC=$&QKS#4fg)H=?{8kzE|dTo0WJXEDqiPMqvnc@S05`+PM z2GoX{9K*Rdm9B)wH%^BsXJnXMpn}>j zWfB?gheRPAwvOXRN-`)WKSsy1coOn2Y4&<j$z3M|=-_~RUItVgk zduz7O&QNMx_pzT%c|*p)?=Y?&*HgHKXXww^KbK|#rBhX`+)1$H_*2_7T%VooERcos zKI)we38wy%qbY)OHtaAd=!(c{EOP#V?FONB#KCY6Ji8m>U9YW4I36Uj!n<^30>X1+ z0!2S80;r;>$UfOayHXK#kShTcH<|>S;;rV|d=Cw^Ufs5Yo}F&aTDr9^I3XN_Rf-*= z9MJ$2q6y8Z8Tx=VxasY#m`-WZofS%b6Zh+9nrqV;+2z5XFK*ZS+A>>gZ-a!SRWK+s z-8NL}2{%aA-mz3n%{@!w4Y_tg8v98bW`jof_B$>EvNKV=mGe2Ab`JKsq9>-h&7PQR zDmf+-prYsJZG}WFYaI#YmaRHE;v4&B`%2eosV4!ASlRP9ju_i% zr>EN9xlKI~+KU~#4-Q}p#+g8#i6TaO?D#RvJ;;SItDoQ&Sr~o=bj=wj9oG^6uEcS0 zYcFy*E%naE>1upUS&ZO91WPX6>o)K4Y3ZTjWz%dtBTAhO#d)LdH z4D?yQhCOv-qE2Nb;*;x5_URSCa`~tNo*g0OuxdPy>t`oAk;{^9krnmOfLc%#w zod-SkIno3tOW$agtR0zIQwDt|fG&9Kh_FZ^PnnDTeiO5`{O6Pfw?i%KNi3nRWro+zxP;l z?pEgvP?lcE>yde0`1ZiZw!Z_BnY=)VkI5hEoU|0NGeeF6+u<+iqN#)|hU`QVJO)g=Cs0;vUKm}|sda0DK=gV#&*HL9J2R5t|J>ZVr!!-SJe zLM{*))%UZBvk)(ZHyi5nep7+5dOzYCkJc$#%zj5b%w0#)D0<&4XYx-ZviL|>EzC~d z(Ze>aIY`$_nNHtLS-JJp>aWH>ysa*s{xv6kD(iuqB_iK)Pm`>`GdP{UE4R6|@*1RU zAwF5n4$#b3Fw{*!2bNh^o*9#;;e*XVaCm`!QvA`VL0;f_dt0~w$dfG^iWuo&ocr(r zooeM-FLEz9h%TFOluZcCZ1tfdIr$e8gb~F>%ORxKQ;WjSK=?uLf!h@^VA+zQO$+@* z?*$syl9j^gq{4+s(8x_We`>Y(Rvml~*R_7lnIW0C6z2CrsgHhA9ZKHvb1!-#=bMzcQb~N7EIE&hwRy1QecE)zje;q z)_hO-w~4W*g+apjMwiwMh0e$Z7%Ob)34rg|9?fr%hG+ni!wSa9lxRTDFuIU}0xx{U z5WGBZ<_7o~Y&`*D<0Vi5u`Ej(y|+()L=NjD21o^7i3)+eo*%p#dGX}<0Ok)dIDD2h z?sREw)WDM?0=^)CCb`wtljDK$fddPYn9uu|p@`39ng#?9B&DYV^TB_P-SHX;oeH4f zF%GN@fKbTBJTcIVaQ+Tg+jckClBY5Tzy!u;^sG?`%aD9^Pg`Ix@S;lr_*=AYVM-V8 z3QQ;|R*$skf5Yk6|I@3-Ixmy_L(cLKaxQP31EJGpKhQxt^OX5N@H4B|v*D1pSsKX# zVD94E!h}@6d{Ytj@DyU12Q6U$gt6HFv9BgU0tqO)DnIHMr~vdHl_y6EiTM?Pd4?hg z+V$rEK7s&7pi2)$ncTd{H_d8!`d63|sz67oGK&M>Yb1F_fR3GSO_G4}_hnZnN;ClH zD+T9N`NSN=2Yvz$_$RH3%ni3e%0G;M%On7-3ycIt^CMR4Z0X`3J*W|IKQIduWbkoJ zX}kmmAZR6Hkx&OM_Y`D20DkrCRcR;y1-#w{ZBg#-eeLzaLP zVqn0WC1A{S@lt@KftqywLW(|pPzXRu4mi1r37U&51ois2*#9l||9`AK;>_;!`gTIy zkhSZ2v;Q5fGEiCp;|JA&4_({)EgB=IZKgR1q6bV$Y8aY0xpMumuvGcr(4w=@7{G+- zasb2W=QfY22JCwa>%W?%9#TCKIHt+1;pCW-v4z_%2DpV~?P2L81NN+ioZ6jw?G^1T z&mB~JL_sr0_P zupvfsG|6+JXMpKGe?s(}{-F$nC-*2QeI!RHE?M8*1xd$<5U}oSc`7!Dus(rfq98UZ zArvlbNJsG2!ef>Qa3R6yvz%R8Sp&EIYfdU4$2jovER#9$eOgGc?-~7trkUHr!m@OC z%ssTdA{**_m@3b5RWktsTEkvWbSnJ~b1)zL$P4tdkI2A-9)SBK{J(75?1MT{s|iQ7 zd{I71q9Y>(tFUxEjl0P0l0-^b5YCsiO9Z%pi(EA)vo3I7Yck5JF=$pKFqP6*7BctR z?~Ir4&E;@FJe0fxj|;i#BeGl`KH4nianXhce{`h}f4%U<{$2?6HijaQeqt2o5*8SwZ6rfSG7~{S%fpi{MlT^x~Ezp z@=O=g9q*_@UbDzZh8GFI4oloR3&JQ^+2p{>E8lucvlcukLhpCuw44~(rK(DlXjYH4-E zoDIb2vn6G}b2KDe-J^m@~e?^c8n=iqQ`UdtA5RMiIW&%pI1owsS=Ge4!F_URYd6T zt4-WGblfSA=Vr{|5Hmf%51Xl2kUsb=38ZTD&xDS>^-i~nit7s+99>Z?^`XRI0!Pp5 z_&{s)GEZ6}^m#TDRu&vf>&V%<_{eplQqM_5ABd=(b|Ak_Aa;wbt2bAMVgVs)iV2+V zfEveNX}tq5bTD&5u*&Dzb>HU0fT2N4L!ZN!6w*kWv^|!&m(U{2(}-SAViG$8mcLO` zn1n8Zfo=A)Z+IgYPO=J^X?Jh@-s*sB_)!BbUEYV*h%(P79&s9W!(0P}Z6q<$eWYfQ zwY2E3x;!Bac%kC5l-nTqw*;dlIvmO%xgaLC_!h$czSs|9TIbxGjhtQAF_sB|@6nFt zEK?J<38E))Nd}dEKvY>Yva#sUVGKiQ+iYgdTr0jo|h|o)HkeLC^U-qR+ z@L1CJBjiezTTd2`9p{bB2HTl}nxz!Y-Y$MK%Or3V+Inie_bkpg?AU8w(6m<**rN;W zm6-5Cm)V93FQ2^FHTD(OP(u=i2WeRYw>Lkb*qEroJNEe(a@IBKg|aaS8_S!3Kw z^;ts^O46fY50HEyvJP6mPLB%CmZB8WCL@mRxRh+67c_QM^#p?ABZY*(Tjf4I8bZu@ zT>SKWik=?xDA&(|G~x5!b0EwnRW-nQm~j#KF^7?==h3Vio)tq63gpreMa*_s{PjLH z`qBH03!lKzt_M#-7jFejh(3d9PZ$|kWjkda3^Ne8W~pT7Q&KY92Zbn$QtRWAAH-}Uo<`O|-&?LV;g-+cR@=*j;p4Jqmb z$11d}vJzli!nS(2|6!f~Fp>ZK-i{U|XDmYCJe+WUU;R#^^)LdjhMx^mOPGL5K8~~Z zzp(*Qq&;?`A56GGI21&E_$*N$)m1|f5>!qp(>7li?Zd)|rT%7Ne_hf)9MeC&19qGb z{5odIAplGs;!L|)}UktmATp3*0BS3&d7BFWx>JL zsC8q@tJ!+f>WiS6y#uCF?>BKG!pi@O1pOF^*RR7cRw}h>vH1*r%s_-UOy(hoIx7D< zxW2uw*Oz~y_dm^J9Q|_)x?mD-zlN8q=+abZI*~^SxP8U zkuBM&>?8{PuD8CQ`@TQl`+lC|_Z-Lb&vP7~<0!mm-pjR|*Lj_<*ZDf%=gmy?80op_ z4;(naXrPa>IB{%Jb zvoLuKOhyE*DK8Ix$jM0|oOY-dRB${HYljh{xIBT+!a34Z8Ah1t~ZJd@P36#~2%lz_h{N zcyC`E_@#$)^9_J5(e?@s@B^RdB4JW8QgZUL@=~($NbrRv)&m>t{a&AS=m1qdjD$-W#Kqk83fYptRf8Ob{6683Og$U_mDj+ zD-XvhAUtp~;NPE{Dp(U;-H-utx@K~+VWy!XFfCsrT}3$$w4ELd21nRq?CsI6CIJeX zy6)a4uGU(1hDZ|+lCLG)!^a%1V`qa$+xi)}Q>@&z;c)j59g?Ln77mS(C(abifR>z< zyqB51OqjJl+{Z8k7wm1~Ye4e!H?$|(856PAcE&nLi%>XP%gh63gh1=~1)@XD3~)ML zc6!>zmO+a4*17=%il!&g+sDAz)PoR+@gjuzd68wkt#Q5v!JcpeRt8Oi`m^cXhThXv{eus=VccRP64Oe zW4(NIjlmdM5xqn7Y^*8Z;{YvPeTt`*KMID|H$;)b{Paw8z{g|-Yh5D+S3he7Q~=i9 z6N^U~;@#|#mPBm>IaHW7-h`xMg+gihT6&vdy|e>$wG<5T6mxeggs*`aQB%(})I%#Y zR7Tc@;Af_b@iVbDGQpF{w!ReI5T76{%EG|g+QX7;DQkh(m($U=u-EZG8|q+PZN7Qp9@&1)R@cilj9TOGR~g0F&` zmZ^=qwP8qrZ;&5BJ|w`#Bv{Ke$iNgv4uBiu0yJR+Yhb-vBvhE64Ooj>#(o~=x^kdV zA!aaQn2fKxjh4Gyptr1oLWsA87D*R;W2uFL+Y!BiRhY}m`5NHegM49b#+G_^D6)*N zo3=b2>+O!mo8ZlYtxzG_x^F_ntFzyz> zZ)urZQf%zN??4+XE2JjLSXalw3hAkh!O0ug1!AEtA>b&vpg>HRp=U6T7z7?`s7(k) z8$+Gc!Ga~{uMIN_!&!Oh1j9nD(N<`EaE+Umg^xMG4ICI~8!JN3@gU1x6(0kYq9mL-Dotf@#T{`{=?vG!bN&ES!YZMH0a07z>1_wt~F{N>|U&NY4sxLpG2% zHHSksBc}+BqOFN;7)4IOfMn!Ok+t&nAbA*)0~Oumbzs^AjJ$y^!Pd{*%uWWU=!t+4 zY;i=e7PRoD6a`}gdq0Cf1A<5_yeefs?PXl>u2oe{D!6Ln2 z@NIkbJ>Tq($(O*YygtGBvbx*CN=+5Eap`khs($9VhT{4w67U{iZVZJz+7zCOw<2&EtD zhI?NeC-+h_3+}sU;JH4{?WsO$_Dk=VotCbTth&A^O|N zYXvCSDwxZ{{V2xZbTDDMBzw7xjRK5!7Ffv99Zn*0+Uf#ZJ#op7C%+^>oU4hS4DKxtygP75~< zKRMR1zALGlPo|;^67fgT2`(9q3hJt~fBK|~n&1|G)P8t!X-TU6lfmBH`{v()Id9Z9 zn|_dL!jrxd6rcMWhxR|Le;-;F?(uT#$LHwZ)xSS4)lM;S%ecg-ZoJjr?6+~o@&Eda z6qM=5SlVUqsXho@9;^BJ_5PfkVT1n-(>E{FrYsB9BJtJz*1k*Qj3?e~zdOL<(5KV4 zU7SS!SgLWuB7AEiInv2e(>M8?s#DtK^XW~Izmje*RDX1uNa(L4%#j$rB}>po>}}7H z=5B-zd*8j`(S4@s;`imx&d3KWh@QK%ckf*>O&MD6Rd3!lc@l)$T6-m&VcQ(FyR-9N zm%$<2_VRY|%dZUAr!Ozgn7r!zSXO9xV4Q>>VaBANrfsK~us zeENBBIqKJ<=i`Po!zMoRatLd#X8~nlI@45Y!{TU((VXQc%$)yo=BP2soNw=13eee6%lvZEloaW7tB&gTVu*W-1hj= zi{gT4*yyfG7RMpUUTrC*cU@_h^NX~Thut%cIUXrp7})73uzqPVo0*#ar1r{;+p9~0 ziK@SA=k-%9v$tQ(%5(CbpApMDRZF!n))?+O)*srTqZfJcy7y&Rp(9Z)lkR&**)d)n z=!(+9xf2}ObAp__vM`wovQ^VAl?#>KqO<2BLmi19I!do`h_83*=)tdpd9R~B#!8~lou2C?$62i7_xnA&s&TRt9A6oEa`+%8ub<{agO1xO24XC=^JZo~ z1;$+w!F_!yd}6|N;5az1Mqs@7*4a||FS%K#=?+tI{?#NYrRyDsaHpVS|Mwi!#plx> zH}4ILUiT8>4ro{%o(z5B7PYgg_ieDk&GMxw<%-z>v9kH`d%4aLtBJ&b;6*@sS%BnbmjR zdgKSaB>k{_$uxXM=|ISG;}0h3!^0sT&n=eTN{5ZjKAp*N7&cK}Jf=3aT30=6tnkSl z7;{tOkFjvX`fK=H`nTog-(xI6>vM(C!x8ImJbP8vFB+??wzn)hNAJovoLmQ%S;MF> z`t}*|iz{uWm9^aq4m)>NW#F9_%kDp_^n3bgAYElmKM$Y$ zZ0Iw6#gVrZZ9f*7pSb<{ZWgLT^~WfC)Z?8oik)CnwdJZ%uL3}lI zX>_O{?5p^QSKmg^4UnlM00~vNjvGr>B%KOcJ>@9ilk3l2c00b3Y@c zRk>Bjfid}CIXSI-A68q3TbSj#bxSB6VdQd8<_S(|+@8%tY!14O#2h@-y6uu_jHm#{ z@WI*^d)ST`C>6`u&naW7wtd^LX2@f6seXC3U>VQkF^1ji+`03Hy8l-ns_d=VdL`xu6%v+uR$# zfG2ZS4#ne;)fX$T5g7sGYUwL&$5{j)ZseWm)&Tt~y5{W46ECEgWVyRbkyT zS&hxnLt((I1D65nhh}!_0>qy*DsX@`$?6p%xmt<<@1u{~Y#?EOc zPF;le(3W6^!5RFDS~Fj2r_zr)CY+vL`e13wXv1{onhF@TSayuwD+1W_THR62r*9A~ z#Ja67r2FhiJ)1B7J#jPc93Mh_U0l9TvpPqz--x)e{8`=6<&8`mL(N=4coI9(YS{O2 zBf?Sqam}y~_T3~;J4!<&OVX2CIv{nHJ}iz-lys8nMiKez zIrcvHRNj3qQv);U6utZ1EcW!(-@iWp)>69{wW2J_4;pXs>KC{-$83>-=(STuW0(h= z_FhGS*t8YL@WpDsnQR-T$e<;Gj~I5c?CH7P47HzPfeGtIAxqb^!CscO!pm97(>r2= zoGWiQ#JrV1<@1J+ecqj^Y4s&{TixPV_%h=h*YHd306bU%dgIojN-41f!O3gUmTwZf zt6z=_T~MsG_1GH*mYpWSv@^Za(NI6=nmRP#5Wp)_swO=a`Qy{XPVLrYYI17S?7+p3 zjI6xWof)*|8fy~Vhc0a}m$*Z6PC~vPHpF)|tVHlAEysYc3N2L>} zH7{26C`oRZmS6|Vv844k3M2mYiyYK6XdaeWwe56+*ye+~L!O0pnazE7EqrBV@ zNY#bv04~xg(>b37#)LRiiy%%HYtu0HwRA=RXTThn%^pJHn{tc`9Cy^T%1q^Gm&kS~ zySA`8A>`h{8~Ow-Rd>pfR!~vVCg^b2?YP01@ciFRyPG32b>^GNsp3y6FEEwA$w6%^ z6kpj+d>N3Znq+ik`a*PJ)Xy(ie|y3kdF{gqhH7)#_;git*;VXaKC(?+Y<}@C}Ve--IDJftd5ycC= z$?>TSwzmGFxcKCK9HWr%{kYosErns^u}Xr7_ARP|rsFOHJ;=HIa?W^m)$KW&E9aS_ zN>k{`?HP&u5;)8;RU7Syp30LpXRpnVCzz)?uZT>!$jOvNE!=DDzAKi^Bvrs7+`$)) zAE%FI5zWu?-r+V0w7O_E)6nvQ!;y+0dPMZBC{5Xez?A!*60VNtEH4`uW!n*Bq_b&r zG-}IVNl+W@G}vaA5H{z7~PO+jsJsB@Cv^f&MkEr-~eNHi>pN-d%w(539?_eNndTkeqrekMky>|ls5+xw3 zP$PHb*EKTxnK_-F%$3+E;IgL$e4f)>MrG$#_}<`T9Ur%8To`5PYq{M`ebIKj-59Cl zdtts7c;G{dr_v=Y4Q1uBj%gfpQSH<8KHAs9G`*0XyWI3kD(h_}yV^{F{jRo9lNh^a_W_LWBPmxq&If=>N(hO#ExpMP?z9ou=WW+Fd<=dIL z2&QH+8;|0tqGZ0ZPU(c&)O2&xl6$F4JvuuRU0JG1XU{X4%;!J0b(+aW(iOaH?+V~FZ`rXFSCZ&uyv4@*?cr0W+Ch&Y z#Qp1-3+d)F1r3(58u>yVto$39el7uN^DdGHyUC8nEQ4l*@~je#h~*R6C7k5s@ky!( zOmX~-djj*w$=bkp=KSUPc---_6I*)Usnb6yNzV!?RW=@d5E~?P*YOP(>*=8d8r^G` zMVLM?;k?VZPSML=;Y~I>xB=|yJ%x*-Mc3so;~lXsfOWlhsPx;Z+*c!7`mk*@iw zV})7uI2*h*KXVO=PvwLgjUP>ZxxL=0H7`VMbAZ)?Tn;%;0 zRDR~Fd|6rk+q!biS3)k^k(M6461+z4S+|kDD|n2iT$7G+_N1O+uS)7kq(AMu*ISoH zFaB%9!W#rxMm>%ymF^l-JT0&j-3&(M5k2l*mGoAJW*LJ!w{ILZt)Vh28V~QG!Q5BO zk)TC$=37?!99|zO4enLb+dOB+TO!eY0@Ihv?{IyNg*}_`*VC~u$=Bhu5)x;IZV)fN zsiXG~mNj0;r$_?=uKOwxoBfj66VGWG}U)rBSF2=6ZqDf7=iGkkl! zjIdEs+ZvC*q#_!qF^4iPABv*Y&^TijC1^MLNWb&?Da-3E=BR_}8d0TU-FP^2UX}il zgOiFi_^(rCF%}9wslV5Sdk_<5LT#Lz_ye>!nFMN^Ts&7Xtfh9#en>qM?j3lgg?V6J z0^R#`G#JgobBYS~F87kyn+orXw_!)xCv0yv3PW)rk&1pYbY zc8RnuvGC~HE4rna&w?#ya~v6hqzp^4t%xfQ9??wMrjJI4UUw$a*!+IY?zmOSbAe?- zD_)Y^GlhAStHPS4B6={WC7>`BUuHXehtyE--JStvbj#YHG$zxpe{d z2=uxmqB$@&VQ_*frR(sV{LgW%T*C1=8EU$y6(r}mV>Fc)Xb4=K_SLf%J#OtWDcG^w z*V}fE_`iPa=ptA<_`5aW*3X7lIvkVdchULlwXb}i&U?Mdh(C2u;@h*Dgx-5~ys+Wh6u(ggbE-(AQDYoTu_3BQH86tB9ke=H4l?##At@MdPd zpYbBJ=it`M=Bmi#vuT&NGlwiDG-}NjZ&6*W)x-$aX05wg>gWl_a^LuA2$0>H8FgP# zyEzAF14gv3Xf2U3RfQ|YI5E3-*F2;5{i6rP!ltjg#uV-|VWNjHsWEr1w|qjD1rqwq z45007K4x+=S?gBC5bmN|sMxaUqUzUsM) znLnYJCcmp2dBPkE;R)D(;L=;S=DnO|9n`O+rVqHy>UjYi{5J4{D-;4Bm@L-Tyhj0H zD*)!}ZzLhrGcQ5_gdWH`(oR#R~(%o%pj%r>J?*R309 zN<`-Y`o%_QMIWdBd%8C{On7CU#CQMA)L1H@UR*cU%^L9v%fBE-r!nmN&#QOSO4h<= zW;HL5={GP7gdf70M{hJB-*0Cfd3xdb>9LIVJe%De_Ck1rr3WCgCK`nRCE_mjlOm>8 z2xBhm)pldxQ`!2w`{VOxC(2AVv4O`}^~-RzFL?;F=`kW?twc_p0L<20gkemZ&N#dD zRFEnEm zFl$u2V=nqrhgCn-hh|I}DeEPgHkfQb`}WA!(=>|gz$(U8NUlFE{d#@f)S>Fr-qeM5 zPI>#nkC@wQRrg=q7f?EJ<>Po|%`*@RBz^q;lgl^i{HrjJhp9F`g%N9=FRgS%6}O@) zJ4KIvyRYL1I6==xuOfn*+NX{Nu+8{9F`6r6FrKqgajKSkoW{b7QL=px0oa8^PAA%I z=74P>89?eKe>?)%yDs4zOr=Bz-~{|4oIlJ)_(aWKczt>CBbt z=^G|FwS79ZIi*1&b-r}AKN>}2P<-k;U}FX|)c0(2%Q^sFhDg`V7o5(vcTH~CuREmr zqb-z33ts1*5ydyI7@{?l3a_Ipir4?u2B9?{0C6YMUt-&mhUDHGWzw9f`#$uJQhI0p z)tgjN;?8(HGsjwyy>ZOTWUoV1J$Df?2rmA~h?8lVg>Lh>$?Vj0p7WFEIQe5?yMw8P zX&b_UO}7hg35D5;I%Vq;c_`DDNr2NGq2Z5kfxnTnWTnigolb~Z7cX)@_Wbx9`rs9e z24{xSS21M}TN}zh2G?w*zNFtuH5|mvS+;f>yp^9r#~Fy%qH&Re#i{9W?C(GI(i|*A zt+iA?XjF3Ux;{h>s+&(qZj@VN)#JhLYy*Vt_wuA?R(g=I-c1qeeAAw^0fF%dZ$^9n z`vNiZE<=y->-OFfymoBePbPE)?~F{Rg623!bPv?8m*7tt3m3pvyL9xp@`I(>bZ#1{ zZ=wXHvK@M^T9m-AxpcMPf0x4aS+(Hx@~va(iiL)kU*0)95#i1C;a0y!)DOefFPmwO zbf@peJVF-I=RQf1Qg#-KbADG8DeOIY{yTrt$C8IwOK%T>Wv z%jCEX;2t^R$UWcG4A6%trVG=t>m~X_66ud|lUClSurVzc3$Nb(xwP8Oj*mGVT`ZBv&>%{I-cHF6IBW{0sD$2c2M73kI?RSCtjoQuOB7RI;8ET6=$KEKpR#@&00nYYRAuDFMkSYLYZjLCI6_m zp}+D0=2t6DJ6%G0zm562flyJLL$?WjX9B%=f*e=>`DEQvZaLpnr`y&s`i6ASs`NL= z&ObL=Sm%T}E^0kr*7f3yxb>-}=W3tSj4|EdvXeLcYd`LGh2UyN1@$rnRnKWpU0;^K zrYA?vKDA<5NxMyN$m+MF$IrUM#TTZ5#{8at z-(%X>06KWqEoe8Ssr^OsgIib6+W<4Pv-=uUee_^6chR|(G!l)nvu=Hsf{06Vc!{{o zQ$Lo^ulf)LQeo!W9WIohPgOk*F24;*<4z2?lRuPtBpz z$J6hetDo0bS#1heS*1SyT6NB{g^gqA+#GtEIxCP2rnu;KuvHO)ZSD=Lmn0Q#_<6rI z!;T{F!MW7*Y=`uVF-niIKYG=FALD4!@;_yTzE*Y)TQ!%iKz;CgM~{n?D>oITNI9tK zm{Me?(&HW*w{+y4)N~I|bK`WMkNDq_4Nq9i9`kHffF?+W$~FWwwQDr)xRtE~-$UzY zmU(Mfhqfizi1uLi>9TWh?QoD7!spm|$eN&VzFjkL7?oeC2tF&_lE_z%S140p_{p z%bmO)zOZ@0JVFnWTOMEk3+t>6@>vUjj;={l7#nN?%#i@!=8z|U9%> zX*{5u1KdA8TuMEU`P5tfYpH%De7Vsxo`#v}Td!)^Mmcc(qyCNGt-VK27e>5847wcR z#~h*lu%2q8B;@Y!Z+&D)lZ-6mHdXze74mJs@AjjBrfm=9nJi1K!_#03DFkmtSqF*`5uKEkb!VApN$ySuI zP|=~)%+uU0RX>=nyyPYM@W~Y@Mc1SLdq;pI!CgEROT$OoeF{*Mp(obwUuM0F&K=@(_Y1?Z;a^??0@e4>DH${y~#0UnQ;r z0^Zjbv)s-k+5j{EA3GZ##Bv-X&cb`9ZtOAUfY zs9SG@j(G!?>4EC6g{q0(ulFDGrGNWy&$5&Vm}te8f-dS-k4JU0m1k=PTYFU2UkfnI zhjyQR$`C1VrZf(d%Cvc%2jo38cZ|U1KjDi=wpe|&MCE$sCzO(D`*Hu#GanWa(0Y3o zb&R?fMt6|2RgKgB2m7|mR(b0=Dg3x zSLOC>ydZrQmP?k@p_%Fd#)yqu0+}mYt)dGWFD4$5Lnr7Jz~U0pB`e2 zJT-JHL(K<$wi;7@_oqw?HTxh)ePQ$bYV2%%uy|3c9DEI+^vn`9#xbesT{GDW^*(*E z2Y!N7LF+p@zsHT6^l{1bh9&bZ28T6KJfOd$DKq&HOvZ$k>6i)=%syR(8cy-JnI9?H z0J4VSKYW5Q_4?SY^78^W*HG2I?zW?B%3w&jxg|Lr@D*>rD}0V{1IabZ!Boxun35|6 zs^Lk?08bCas-C=|99jch%oyNvCd`IhDm}!DL5zp3OOT+pV*2R>G3S74uG#zfEtxJq zYS1&bT;sU_6$}5UNZUKw&>SApAa--OCwu(YUs?d)$B9wMn##rUHl`xO*xGyeYbjbg zI`vo*a~>^~gej(8H}Qo9RsV(9Lo`RsHLa$=@@Ed0w#{+r zJse8LSG_@Ko)Vy!I7}B6I7geQK!r<3&eCdu)bK$08->p*crEl;QN&##HQSnNfvt4$DK2i}zs#FJy1%?#Ycz#(@5s^iHy}sG-_8_WH40yDSVzzY z-{ViDky1J(_NkQW;QkUa*ZsS{sAzMo5}7QjtT2Lgp32l`W}VvRr=?%;radHTo?|_@ zbuIFxBIZO{hYxxug<<6e$baf7?R4-*GsN9~b|tpW-Y<2zd^9be#SGj^ z`1A^T^x^(I(BAy}JfK>G6@s-iCSp_5^PfuxJ*9u_NJI(6T<7VRvN)lJ7osgyF<)e( z>;CkL?jY3pV5;W!PZIl$I`Hp}dT`x;C7*$B^Tj!#hBJ>IE`p7n!Su+(@5M+2Wo?0B z$w%DH66)B+YrP7h(D<#r(M)Z;|L?8dn>z9|FL*>e^o|k8;F3V@r>4`Y;NZKb0nXZF zcV^zH9uB@UFBco`Kd3Y$$DA;gJ^SBo3woo!mG0kGrcUaYvf3dJ9LeCB+r&7jf9C2l z-%YaPi|^qFiNENnA+Cnci7(sQ$=JNqbXh%>B~>evTIVkUHw&D1&Q`Go-+@MpC64(T z%f8hnXmNKQ9}`M78^=2D+)%a7fBIo9;k|u8?#xM^22LD6mQ^DG&8j41Sw3Wryo8bg2ejCiJ-3$g;q*i7db0drB3i!^`uU*h%Oww1|Hq&;6pKYWZieC zI-RgPJ`TV(Y`+vVNtpYik4$TW(EMaQ_AkLmhrEiff(ah@}aJb0xwMcBxqr{i%)?XOCm)5WOtD1CcSes80qgYRhQV>3Z4L3~$8 zrnGDU#2rP?TJC@4v~`J2FxbXon)%*X3^y4s$VeU?F=j?i6Lpqu#YmZ7>Ct+fM!yOKT2wmWC{#Gnv@!(DIL zZGW!XS^s^ma_TR$Q_(X!5~r!7I-ivc>&sud^Z&y;eeiqg|Ecxi*E8zDHk{!F)>(RkU9$3fqNRAhR>}Mr2#zB$4p9<|2V&EEw^b|aV@W_!%*^} zoMG=dB1-?k+O5z&RzcaryzjO#Z`*Z(9VSwNN9YuPTb^AkOyFMatM>i4^1LahJsBVc z1M8>_5DuNXb5pw}$23bTLvb=CPFMM7?ZerS4b668W4eO~Qq{qFYi(Nwezk@d%Vd3L z^N|z3B~taz>)b6c!qm675<;(EgeLV-=hCl0V_o1+ZIrI(XHhXJlo@uI>+#CF#(Rf& zfkk}@KclytolbjReEt%E`=U};M(CY^%QZBKO?p^;H-2ucyoiW-qobE_N>V+Hlb4mv z@~z+v)5FG&r=A{fss+FxpNbL6O(!OAtfmX);3+bNl`Fu-YhB6aZv?&hpKhEe3`LJD z+z+oo-d(?P?N)#>n{dLow&;HlF3^zDUwYNtU(!u<;+->&|N zE${w4rqNCQ4%A`8g2_CtcMGGQJp}NKG<(f+m^_u{2_owZ2+Y&~aRPUfDj=`8XE+JMtH3qPBYE&y5O^ z9NX3$$6$kB+jE6}0FXWb;NXoCah|2s4v^%F0MF5OsP3$^Z33!$PA#E%8JNYbt-hu;-o5PY@1F!Eun;jf(kzVnKKJfkfBA8(v04~5Y(^ReQ6F}}Y zlNThD1Pg!yQ^uefj06clY?@VBfOirMqD!)^E8CdWM}de(1VD(73jjt{0wnVeAkC(g zPXKCjBpl5|W-1H00qF-r<`y6(Try3RyjhSbv0y8G50NCPXKQi+k?_VN&ks?*^z0k{ zRvLo725=FTk-OhNl7L7wIqL2;kL!W2DRVb$*4KM(C}+d~ovB(SI95ox3xvJO5qltl zgb=6qdlTh{=OR{Lw1l?zLi_-JB`E2n2d%5P?!GDLdR$Pwvu_H)5B_cnDy_wpGDGf} zLtp{|x}}>VPE3NSdI}i*L7?e52MPOt_*Y!1Eb<-)@6ZpRB{VIymMQuTW*7k?cf4wQe-1i4B}c_LO?YWl4m5|{6(ECBw2^!y;rhnSFen?~dD)$Nc(BB5S7!S3_!*CLyeyvh z2*i$T61)}HAs1g!X*nA7ClYl2cSEv%OGpScd;GN-!O4 zYvH?W=XpauB^}a8VVu2ESS18x_uC>1vs{q<(-yb?-WHTzO@qak-#|CzSJkT)UP^j9 z=-l#-PRI?&l$i=wd4Hi`vYNO_0BDb9r`s0_8&EXmgYvJ(bRZ zxFm!cM1_4k7diwqlHMQ+#I2+$`weCRF#32HC{wN12{~?+FF(m`)pv^KQD4@a${9V( z>G*c@b9D3wkfFAIk~?#sflE$oP!%MoG>HP%wgcXqi`DyK*s7#hXyHDpoBTIa$HmJ5 zBz@F59JayZ0ZS@FVBy6JxU7}Hp6k8`z~m$8Q^?2iiY!BIQL`1gpi02?Bq+O-PgrBp zCmyl?ybj5?o5MHeS+&N8cv4tINnB6wTT7*_oQ$k!IVP)B5@$#o`cXzIu6SUse@mcr zuv`g<{|~ol839&9heb1bZ`-rBKB?q7Sb{I50AA--iy#jwtO3d0$vg`?(!>zo8=w8b zm*k!~ska{_Z2#Nf3t_8A82JVg$2A$m7{mRC4&Ut>csJJ{3FGJ%#}KcdYc!ke#_tmm zjK%&(ss4<@h5w3eI~NA02N!58M5was1x=Wwl=3AW>S(Ak3%;V{7(+qc+BY#;cvDSbr#l)+wE?od|5ho|F&MS9&H3^{X*Snp6?>|7ThH8P? z(6~PXLdGNQO%`B$jfGfix!<5}@;R@(_+Pnfuk)v`0m|0nJ3IUw@zj+DT7R**&?uDPM(ksRTTWXWH1 zxv$l#d%wNN-gc-PlJ5>cN4)yA&S1K9ic5*;Rr&-oi^CME15JwXstU>J>x8;Q(@Yw{ z_t)cTN@H#1x*2-QGw4@AIxTy$geaz-R&eVQo#0zG(ftZ~gS{{5=pgCKi3+t!cG>S-!2r3!38UwcpuUz2tZH`QKL4mcP_rPky-DFrfH4X~$K3@x+BE zF~2t0HOL!8&57f$$R)y5>QJ!BsuB9Z z0ivg*@7&Wmbx69A$GdvyXzV`Cm#B}kFxgK(iQ^y+TcxcQ6tLoZRewAIC4xPmSjOrX zsHVjO-1`&&me&BddxHvF5A7s%fHN1fZJa8g3gG@-L25;Vbk)tlkPKc>rTqQN`;Hu2 zS2~B_PLV}Ov;nE)L8@Qj+uNcxv5eoKiGCmnOcw_g&^%DKu3~_6A#c`3I?l)Mw<{>v za@zb)WR8U*^8)NBfK!N1#P>r$q7VS&BekDE$;l?DIUWRbiqBQ2uzQeVfWqWlwG5z8 z$4gM0xAF;Mnm_`naS>oy1gI#|ZxVo!MuYT5KyB0lq(K3?i4P`V)C3-Vt+q5lz5ta? zs-S*n{p&TvVmLHb?|FX276UQ?EWNA`$HL6^dD9YGz?*)2AS0ophnV1p3W@>BN|a~; zA`v_gX;$6=p#rE7-h<@piTdd)=mjXL)QW>}Kd5b! z#h5>8+_YEz{=u>>2rxtSpw{{`C~qS`5|OyK6IFGy*SjGJ3RFYA2y(|y-XKO^gL2AL z0U;K_jqgy2oV9}=4&=7qM~`df@Z1CMlS9D_?^mIczfxRs0MmHBC3qMl-NRF2zPr zeull~4mh+Lpa&TQQlfBD(<9Zq@)8XO(I|f!W&-NHd{gv_#87eMl=Vl3h-M7IS>yT${204g%T= zlI?DS-X{t60{0O3qSu1XifZ#556~YVI(P_!xU1(la-_Y0a#Tov8rp+iIrlGeX(8!iU!sR`Pmo@8iK_+J)@G?NT7J1=|fS2utYK_Z3Q&qGy+a0k3McKes zosLEmek}m6!ruNuG!M^)Z~*we9$L7GX$7y4shH)op9dPfR9q-|2Kadu^e#qtC z-Rec)f$2k=R?eh%%|*#brDX=y5Z@0stykJaFO;2giVAD?{^LQp>Ljjk?%%wt^e@(k ziMBl93D|Rz4AX`opYyfW-^OTPiXrCV?-uqDqH%9C{KfwuP8?jifF&#d*^Ioa^93O_ z=n_Vz?s_!C{_W>`Z+`n2kd@60x%c?C zwIw#~{hqg9u|5#`hwRt?iwrsy|F4K3~m!^o{ z`YV;psC(18Bwjh#)}vE`w|bKdO%xYTPUzJW`}_Z?oF`PZxAsQ|RrW7-3~?gXurGFS z-0zvnVm8f<)k{xuu0u)G7Az(7{Ga5XtAS*U^8OSU-u`O};#o@@!gW}=hgKjZ(UIht zPa;YCyYP9DhX8M6p7j!XfcR6){f@tS8r zMXJ=ZS5md(anE*Lo>$YZrl+Uf*Ed->TWzmm3u@5!cl!Xvo|Rh}dVG|2AnoK*{`R1X z>j=)5FjO9!_rYA`2c$R@XtnBF1sRZ$ByN8%5J7HdJncvN5EW^7iDp~7kh0M4tG#M7 z5C^h1EGp|mfvH-+&U~@(zWR}w540x35A0`}#K7Vk?pF+2v-w13LGJS7-p;xyclbiZ z;ybi-b=2-p5D-OrK*2l|y&}L{0EUR~jXBGN;*T&^0$Jh+fAkLP-h+fsT!b^z#-d_hvEWD!VzTA~vMK?uS(IXwto!|`brBu8Zifa2KCQIH?XNsb10 z4*_BB$sGN88h#pPKKxuh!9;N{`u8vA6Pa|6Er_pyB|wRtlhNTlfchnSfq2;~!A1UczB}q;DWm{R(b<`XK0p+6S?yn!rgDnb*W|Df88wvgrc|_^6{0OS|yj89Y%oE9h3|T zjT{JvR>>j9H7WW(Djiib0j1mp0K&ZMzn0x^P3Zj$JV8|M%>tn67U0{@y@T%hfnou= zOyx{Ec6jEg;t4RZM^FFIW>%HMoXNe3gkH*|VDczm@tRjNuqB!8W)Q+@=F3o;f!-DY zh1M46v&Vlf_TT@Qaw%7gg(^RR^@OFVYDn%|@ZN`-K&mm28*e@STPXDm+fMrt^A_r! z_6-m+A7PaTA>xtW$a~9D(=LgOnO4>F0(U^`CAUyRg$)+>wg7bX1KF;p05>UPigUfdyY+&{ zz-yP{G&B@pqOzl4S!!y00uyW@bu#tDwNuatTsqy(Scm(PzL$Fn810PtEJp#Rqt}9J zRD%|Z-$|eXfM&JkIn_~Wa~nxh$6Yu_hv1P{;;ud*5apFsqwZLaAO!=7uo zI{x(eSUOaM1{R?OTx~5@-p)YoV4&h7FTCdH2-xS&$vC2whrr>1EOe&vhSV9-el(6e_>e!bjXnE2n zn1MBtYw#}$PD*eZ(7dI9^{7yu~8kcAD8uWGrI0!IzKi>>=s+>B^RODuELTA^O^G!%*S&8Z2&Wx#5j)I43s{R;b?*dNk4h} z|FQSp;av9p|G2&5Eqjy5DkI6t9uXmAZ$d_PviA&8X0~J`-e{1LO(dy=BCE2q_xe81 zclUMw?(6@20e8+MBcU?z#pYQX0ov+vP`B+Z{dFK$VpMtODBdC~)o!X;k zy4-MDjU^PYp0#mV%e}vQs$5rZX`Er;Y}>i*wXdA#^*B}tCo=TsnsAdZdsjZy#2gE! zYmT7}kOrlRYpJjaqd)$FtNdeC-A*>OF#M5oF#8e_DRGTamut(#xZ-m{`z;4 z8jnKj$>|3TXNS4jQ@BZ^=< zLc7r2t~a9Z#rf3eh3lx~duF`WsslL8rJvq!=f>v7Cft=t;M2Ui1Ai+y~zH1g$~w9!{BuWQu_Q(4RD zh>FQ50+gFLurj+G+sJ*s*XPbS@TPw+d|FskJTj?OJkzbD`jcN8N!Bv_U}^NASelZ5 zUtymkqD#k4(F;wcsu4qn$Ntjw(Tv>1&)t+kPrln+-LX+4FxUMNGEYRzcPDNJ+OyLt zL@S@ONq_Hp`Cr&gkX zp1sGFpm7qd6(S;PusG!Yqw_(N_5m^V+c%ha_fm$GA(60mdE8UIjE=U3qq(hxPWvA%}W zdZRL+nv6zoMPQq6+jEL+A104#@;3_G4?|1wRn7hUYI(KuT)Mr7INm6&tH;? zk&7UhjJ}R_NJZEZ6-ccon6ZA(^vY>DQJvZR8`j?qckR_eVxRlfI$x^)5hqK<(x!Fp zRKq1)3lt?0-gXF)3$NCA%5W_mVICL@LhlMBS9^7=qG-ey`1o0dI9@olcY3^qEGGI% zo3iBVYTk*klji8`Ui?8`K;o)r8%55-=qq0o>_HqILq%rf+M2ErQT^$j8QGJcrXVgi z^?E`uKroCnLX6+jZt80Pt}!D+YozW9p1O6|OCy~?9P`MbawPLT@7mciPrGilTsul` z5pM487ugk=5J(WhW0{i^o70!MkYQ;gY2_Fi8%D)@N}bzFNfjFD)?A#+5AK!lGf~m) z@$&euo!_Gr$3Ckjl&pRIT48>08*wUbIoo;lR;6@GY#c5KMQY=~v2~iMGQnKee#&Ccu(H<0$9C_d@~cLPcq<~C7<=aMp$ zCytgW`th$ZTkxyKd`}Bdazk$sP4q00?blvV-DgF;_#P%L#Cl>E(FxsZJB2q}`YY)A z?Edd3wL$hZaSG8DEV{K*oW{mD0j|j_NMyZ9y33aE$S~mTn7r%GCxQ zrw|gIY-1WI=(f+dEyTTT^sLY?rJbU2-DLRure1N=lPe)lBW}Dhz1C^x^Zjo3oe#!$ zzuvydaLwrG^p9BLY}wS*#|!jA;}i<2f%#^Sw_{S7?Y5uYD@qG!BlO5yOz++t{?vM= zp{L~Z{fo0E$WB|D9?RT_?6g0|3vU{+V^$zON#IF?`WHo_X7x<3ORK-nK5q%{K9zE1 zc;eacT(?55%)`&;FZtoge{~*BnyHMC-JB?}^f1b9# zIk$Y_GOt)&&8f(*pC7eH|2KRuhR+dQc+Lg67z#eN5%1K+9E_{%&(dXLpT_TZE`B)i zqM$KddY+{u_V%U%G()kF+NJzkN~H zBHABSM}zbd(a8c}d=s*2@1C<6Qb-C#A$Ywr#MC+&Fq2TIobPY`s=IR*ioWNhCgiTR z7K=U)V=*SP{>Hnu4iLG{=XY(Ks@S4p@l)jvlykfeYSfP^vKsfKQa*9 zgBooS5+ti3+xH9n_YEiysvo0UNK~@Hk$G(5)2&B`Te2pQqWMGoHRMJLbs$1z$jVF)Fg>jpNoYsS0yIuG z60nQ#Is1gPt8e3bL2pjZ@D=jx+7OcDJ~;AJBnc{!7|c+qB3)gKqk;3!@2Y{I0QXmj zBJCrfU}dhH^IM;>0-^N|(S-KWHn3dZ5`iN63guat@5HgR?1j8rKN%Wk3gZ``dMF)K z9$nh{dP5d~jm`yteHg5I@Z^<$z_CyZO=Ta_&@XR+v&9y=WfPRt_Br5!@cLWEfMuW< zV+-1z*aSEPKY0UD&Lh3C##_-wDS#>#fktGRRllVtLq;!@?@Bg60CdOGW6a|3l~o@l z@4d#wq@DWBmAODSeLZF-19B!8farM+wB?|3bY|H!os#5qLg6#v!``4B{5ZsUVUYiR zbO1In=$ZyRt1Vs!Z9?1|VZS`WnkG;!gL&H-_>0N|P*_{7z5=)OF_SdBpU_xK%|5v6 z48G7~snLjX8Bu#8*@s~V@PMsg^%S}*YjOR6rAB@tiVyFS@VaW`Bruy#p_SrZkG2Hf zMG<`;Xj~PK`~1dus>L&g5F!~qGfr~OX`rx3a#OuOIq)Xfy%_@SN^+q={{)6(jXi|6 z8sK!=2x!XfCZ7C>pj`unzyM3E>&~xfc!fxXIkpg=dIBVHE8rT9e!NVx{;(=4yxs!6 z6M7b!>^K&9ESvCkzJgttvD_3IWqTBt+cwg8==0#Q(maB@Jo*kS3?UPMebFc(SBjfw zpJ_+yzxw9XB5+*KI1|NB3~AHsfg?eo7+}C}XcnKLW(4wKuY@&oV-LQz0+%gh?rcI- zCw^$pwjK!sT`n8YJRDWuv#*ap+B0K0AMg1{gKbFKP@qL(3hNnq8iDJ!mFfeg>>b8o(*xsbsKtt!T2e@6U zS~_tUNgIG0Y`Y2%ZUc;2Kz;dXC|54{Gg=W3+EEO7hhmo=$!!qgqpb?3pxr!X9a^qT zYf?-Vlc=-f2Gtiqb2U8R$m08&V#94RupEV&KkYJ0XA(jG+llsi0wd8rFN7 zUN7O@lrPmeDCCP~&j9KRHxD7D*!_%LZ6sU^B56saVI)SZBD-zRnHbHsR!n z;B@ZjqB(hdiqKYiqR8QE_fN+t?jnu_oSVbxYXAcf2!}UCT^fDiO&X{ptfFEb*Y{gGM`97=q4Wvl2KYpB2d$H z0(mF#O%D4+NQ!?Ir<+UaokX!zGv#Y1U*5YQTF%ac(;lzW_(-uKHb|`a=w3wXka`ji) z`axC8`Obw?APgtY{t9@D3piBB^vO53D{8v=3PhS8Bnnw-Q5*}cX*+%?;(+m~5+RPT zi8+#WqiA=63O}X;+9FXRr8d!2{=MmwgOL5L=(WnE@bZp-NBB~Vp;8a5aJIHh zgz*zk^V6|F>a5TESC||gAy7%a(>Hx=3{)}@p6_>ytG0@o#Z=--!T9%{C=Es$^2>AE z$)|~ttXLun^>;)G8eRxPZdJs6v+n&+cDlX}0LmoL^K&!=M?zP{CH&=C7WrE?%a(_y1v?C{h@NjVI8)I0OI3eliH&Q^n`~# z>eeSe0T5jNq!wWOoU!C-Uky{BI~PdSF6$E8u5s>q{f5gLTmH=|yN!0UwFN4SNft%R zoJ1raplemY7@ZfB15l;(LS7g@1bek?cCbzPOEM;vZ5ayT-BXLWqu1a!QLCb97y4)w zdSpvBRcrs4@L3P0(v%+cZMq`n2({W;hU=6JG*zxg*w}+&atKt(L$`{)s z0x4q1(>yY;GKT6K9{72(58$ZJ3JlsNY-+F0Jo1`olO+KiP9Iy0;9phzpMYakJ8+-Etgh(#j-us&FOwixJWE{ z#F-vvf$BTcpxO|1Hjc6&l8(SCe2_L{IZN)!5j7!nDN=y&S|RSC58#LkotAl2J={q8 zMF1IRo&jMf0Q7!g8&6}nYB06Vh&-Qypz7y6vHC7AfgK_T;Q^YX)WZ3DAmR1Gy-&m0-2?o06@RQLEQj`GIv^RTD5?`g!NcNRxwOAb zt-{J&<0yu|VW(-JR7c8tSaHR^d9<7CNca<>{MX}O>jfH4S~Cz}f698eT&z(TUXD{J z_I2isIeo+^0=|nmp0KPxOWbI8zX)K5%ZAtFnbCB(*kRfaNg`(~Espnqg`4AId%`Og zy^G_N);O+L>O-FItDt!Ix#`niam5(9Myd?G1PGL)L=?mc3~eYJYI48gqH_X$eV>(8 zgNFS2@a5$)l^D4sjY)}58I;pjINBehY{{X!m-#4fv6MrqaUUpx4%w%)sij}n`lhWzA$7X{88iVKqR#Qifs5>@dB2N{cDvD zc`@wT2ZjeD38MHQM8G}U{f{FL+<=l?%?+4qw8vu7xU7LM{o#x)f~zDCrG z*k94})%NKxDEN7my{VB;$>5bq?Qazg?;jlV?>ri=<5xA$^BC2(<{eN-TZGm;eXheP zcPqCTyRxRfC(J~2K6TE_OZ<@t-jK#6?-VZCv^2vn&QADXY&~gSb}IOv(*+z*I3o;g z`h0eWK*#ay_vWI0DOhJdy6V~A&%S)={jrk$laaSDTuQF)W4KCYPQy)y0AQ;6l6;9we@wspl+=+!pPJ`6&yz4+rx)~2 zakT67(t6o`wrbKx-zNBp5uM1u&BW}tY`JMY)25jQUp#Np zOpk0_fgdqhk$8UHAn0AK**3Gg(T)U#@RTQO##Q*2&1H$_MQgWC_b0nY_Vzh%`t&Ei zkI?R+u}g8#JWfS1INHO29jP_K+I?3*BGfu>g@4cAYp3FB_ERAqDpu(doX|U(V5ObCgzDy-}shv{m^y^_szHC*%KYRvI!=o{MAZW?@@TEd7ShP!?hMQ zOEuGX*v0u4DNjCG7?lj@%v+H6k|r!lnUc1RkHRWni2iEgNc2#h)pn28ch%{~^srP= zUF5SGw%qILQ!DGKo6;&zLMn(M5_s39V8mRlGau?e_M?Y?5$mOG^odem zG-HCD1Rk4UXOY6yVGIIGgoi;Y{=WpO-%As#zd8JqgLHptNyF|a|1m5d(b=%+yV?wI z+ElI%my%l`$V`kJGxD}YpaQxgFy%HEbymX%EIeN#+P>NGHWIOd5i3CLg(as* zi+8478iSn@N!1L<{{FO-{{%f0auSYT`c{m=LoWXRm;>_vcC-DzGBM@fcj~{JE%U$p zPBqW)NkaPksXgcmD|AaPKUO&cnzI(f$HNb^J=FKWFzkXjc7b$n7&s7J4M7wEGP^UJ zh-a8_*rm4-(Oz|3IQEqAUqlzkQL;eQh!Hbv0=fvN;xQ$K@> zv;ow2VHD#ZpE?=T=czEB@Pjc2($GdtJGUM?{{&jr&MyC4O{AumJ}VU0_=n2ucTU#d zF+go4!e7E~f{}bS-{tY~MbQ0HaUVVXY|MXZ$?0a#_HDJNp>H;kBsloEca?RMBn@y8 z2y*l27Q6d1dl3~wq$Bxb`Uz!6{)-TzIY22;9KEjKICk&Ct#?#{35Nin6j3rbDzYJ+ zmss6+^u6Ete6jf}+SfsLCr87Aq+OM|+7=cCA02+KweLe$ppi^DsD(_%KT~;#!>xS2eIEl4f8bU7YyDgJ=NNKs(}*@iFU!4*RN^ssY37KS z8LtJIV8N}-EzJD}AO}ez@T_V_d z9LM=gPWLx&fvnx9RN55K94nCvlQEpmku+D)5CsrDM>u8%w_y{W<5#Y9)8mIi%~zVo zJ(XI(N`D;?zdSP#QBAB>*wHV-d)eG+e2lX7f#yEZBLEyu5}mRx9k_YP#yVcy<_1CG z6bNW)Q?DAofw3fx{tyJQ0IDI1FcSCbeif!+zyzq4RbM8PDiK^d1Il*`LzzOxCm?T* zmZ5b2el6Y1hggQ=Jpma#N@)~?*=)P7LHwzpOql%n3I+@;Ve@|Fio8@C9cxQH2^gYv z#Nn0~357AIEIssJ zfy6#)j_?u}A`%(Oa|0z&6tOonIrpHOAgbJDxQU20kR{S4LUdoIs^OPp0|ZYhDC1ap zLi$k>iY}%Mf-^oNz|S(FRb))Lv&LJERgg*fXj%)0a-slgFhX#s3dLx0ABesC3OFs; z4~%Vs06D@Ip^j{s}6(}o51&V!-x(>lgB!mN;Z4?RDiBl=sn0-@< z0KYv=bgr#~*2I}orUW$Uc*5f4n53TvYe|0yHUB9+%mq^TUH7dDr9sf*6i41-jq{`X ze#e71DIFlKVypaACZ~N)-XQ=4AXXo2f^mXAp7*CMH+ihy30osmVtekRL-qdVA;1f* zGkohC!sjlTC=uS3lBxA-SylG7(I^*zbPdgIK9isF7YB}u0u*wnlj#cz7?-l*G?Ie= zST;V84;L9nTo{^W)pxwdZ~ige<~_YQay(jpO_2#D{J1!(uyU?`>_Y)9j<<$i=aXBD7-2e<954;eDAVa;_T@ zu=Al7ye;!^f;qA1^SELGOzTA^2U$kHMieuXhz;clq9+%4gC1#p@RpU34{IPb5e|so zS%9aa432o!~g zaFQW*zNS0HR1vh3Nn8vaKeZ5v`f(aVA(>RFG(7-uXjp#v8o{Kf)?Ad>ObuD9c@E5& z=~wQ2y)Q}f`5xN^jxry7(y>t&Aq+uE&@la5maFU~l&3f0V@2g~b;WWvRLTs+ETC-JX&wd9FOmxz zmLB$GyN{jq7@vX8PsqLY9)b%-#o=XWvvhI0w2|(qA#fhMZ~5LeXFRRP!p$)k1f8`< z0hkICbe@w2A)n&Yunu$TG}R%0oG3)Zh~gQtwN7|FOdZdzxXs>z;vf>XmJ{bNM1_Ds z$9E{ZCzmFz1C^+kXMCNcN<1wN)nlaWVgp0ruFs7usc_^U4(T_j?EE-*5DS`RWv?ga zCAkkZ+-w6mXonnP=}QC9v@~SgSGa$uoD)k9!`DY0j(#J&RZSgwG*Cz+U?gK*7Oh$A z;$iBJ!im@uKqmZJFC^bXp-2P9B6W8I)dOZIODf7Lp-gY9$eR@**HzyrT zD?!8Mx>f4}mVnYzl68~UlCbX6tT_u%Xg{{RfnzW`Qa^E8^LJd@J*j{0`2XhkhG5N@ z1Ynv@FMJ&DzF&OoS7O&AcbdL$dPd$TdrrjB?y5^Yn>xXhTCoX8^GiNL>$UbtmwdPdpSB_pQJN`R? z(fywi82^Zb3WvEpGL*R&-&|4mwxQy=FW>W_j>`-1dGA)oCuXDAq_+7Z8gn{hxi&`rt=pc0Qc3u(SErI2rL&{=UJMVlr;=~Ec?SDER>k&=Z0pIX+sdJ$rO27U zK8uK)iPB?}3>4Q+aQ|YPHh0Ti?F`%~@}t*$m2vA@6QffFrPwQX3+ceM5H{AO`}Y3J z&4jI$L9DfJaRY`zlYGZHq~Te)j26^Kvjo&F<(lnn7(4CgRmmPTju7z#lzscCQ~x$4 zmcqiE_q>)dQx39h9xFeU-#n^%wH8wIxb&INg*OuB9(5t#dDN0`;~dA650mU#*7Km% z_PCrxmc+xpeXnR&DYPa%9)=`Kx>+zhNNE=4g^rJr|+15%8jfq=nyPuZEvh zYFIc#S6>`5Yo`c=rB^$~-}Q-q%V3#vz{PGp6Sh#(*{qpEXEJkTQO;{QDSAxzhb5O3 zA?^zC%t|L)lGK=RSIu3PlG9gMX0^P=3GL5_7KY+Z9=qs|h_Q5pnyUfmj?NLpNky3>7)<(cTUo2@^;EARhY=JHvb ze~GfwbIRg;J*-Jt9d(+CnImjD*^Kdx)spEQdmnY%J*#&H?7<5QF&Ugo<2@5P3u0ZL zQbp3sSI>ORi)>&y0j3tRCt8G)MZ+cGn*o3Y8PKw1`WAAR2=+INJd9P6Emjk?rrzdIaIe!%w$_J02r2!NdB<5kpO&MNU3RB_0+kpSJC3f*bmWkj&YPaQwU`af6UUhEK71i_w_`U*P!Nz3}WsRZs>u$K?nb6Hd;$j7_U0 ztY>~5gy_Z7k{O5ASrLl~l)B?_rSc)#3<{#5W)}7NTF*kR#l^5NHsh0(@o@x*=Y^e) z&x}*>GWN3FwQ;q2-hGr8s-#NV`m86GQrp+AKUtjk;9M2Ay;myvv#&osp2OvfMhOpj znu?fwUv2#PL`4ZVn!)`92jRZ9`?#*SLik6Ddio&-qG6}kJJbmo1u6Y|L$(W2{4D#P zgZzw8>g} z<#jZww1pw#rx0Zuli1A6k>F66)na@H$S`4R)eR!b)(&N&_-`gW;llHVP0V>HE7M-b ztb~g*f^;#L%9-`9)S+}MNUaV?2LYsHizFxvuRAR%av=O1ZSvzNNm-SY>i)1D=1HeK zfG=5yJ81Oa%?on!Ls+5OjrQ-s(SSQOapB(9j@}_OAZ;EO=qAI;vBDy>`tQv{d$?`L zWwksX=pC2XK|x^l5L>(=#(==_p%gL#P%!(K_??;j6CpRG%LlVZx=QvM6MeYF2!bNI z;ZHG_=*EPXG4KhR#*ou`*T`72_xF!cevn>Vg;=nTS>A`ntdJZk-lc0baZ zuM4j}SPq$Zt*4Xn@#GX1m7Q?+IA#4P^IZX7Tj*6-tcMvULyuRFItz;;V|y2BTKMA= zHPo)${pdI-@C2FyDgVpnwk%lT;#|QI_^IRs>^~1O*X&V0Ck<`+P)I!1ey)8#Lj|RP z^LDFaQnubu_DmO6AasH4HhBzf{uTk(?O6y*=zYv9(#Lhr>#{KWDKW(mK6EKnS`Ls3 z7Y!1Q@iC{ku&!+gWbYK^hbQ4!Ee~U#!+RZ@mr-iWeunq^cwua^!cd>PquqJUiIzAEU(|?&z(%fAP{C=*){7P{h2rFKr>}$oM%Tc%H1nIsv z>Us%4ho|$_eV5Dnb{V74wYl4~4Dd~Z-=1E$x^raH-`~=9&Bq9{ubnJ=JL@XdNcCJat?wjU&5p<~0lrT0)YV$-l zuC>v1YMixGJjz~=!LshJ@QV=rc>YPWrS%0e-OFuC0UEd*Du&bjrbNfePgIQMXDKb1 z?sJwX^fRmWW~Cl_tUzv7M;+>V44wHKw#leEl+?=<&bBPr)@6Uv`aIe{s-AJMvOe2T zvD{tAshfg`IdCJJWl;~U{R~di56y%p)Jh6Q;K%EAe%BfKS$Ca4_sg%P{^8JoU-Mgm zEQ=$zq7cV-ky z1&oy-UyuqDiImym_B;}kFwTEcpdpzEDk@r**ruvFBh1T=iG}W=j%qmlJARYHYB)g& zbD0;iLoupn&A@Pz(23k}3g75llE`IxpExliyk33N1BEu~7e6_9qkV$KBUQcG*}+ zmsYXwl%aOcYOg=h7Z-%DP(2GpV^|t0GEMF~eUKJA%>o~qoGA2VW!)yOUk0@DjPEKwj zUOAfgtRa-UMqXsJ180UGy=hJiZ%0J1ZlLCgLclZD?;XAE6W2#f&+TVZ8Pto9R6MM^ zGCdEWj<=_)yjBYY%gbNRAMYL2D8Sro9?@I)4aJyci8JusvXi)0vpAf}7&J?RLeTzg z_$D4RSkgkf?BAloZG7H;bXoFP@eyWVmD6<&|V!>YFB5I5sUMI3v$UGf~>x*8R!65~qF{1) za%ul>d@ugRE&3ID1XX1;xU9!7Z9y+e?>EL4xUMr3xOVt$3(Pae>vkln06?8hH{U_b zbF{&O0lix}=LwZpFI2s{A>P#IyBHXL*;CGb+G#0Vbx)ysEu{Nbfwb4@gRDDDm;TTK zbRO~Qkmtx=(duTqaIDPdN{LuGEoQTNDkp)C326zR5RjzL9|Lg3x8;1Nrf5~b0gRXp zm>TK7# z7UC=Xi^ZlLKO@J3Q1laEbDyt=+29*Z2Y{Eak02@R4z75e5FWw`_H?1O^(Q<=6Y?{9J9M73hxQ7S6r==h5jR2-iO-Y2e?%PpiAwjlX|9|C*M}QbO?~y5XEK1FXaOxAGbs8V=K2E? z41l(fVqfp4$f^Q)%pZRLee%*Pxl#N&;{^IgFRJ^2t3_<31#BGfoZa#Md>1Ny0 z;}&~g23tU5NlR^Y)%NqL^m(`3^n%_KnPETkB@9Ne;AStesVB=2j zjVQQ>+yPTeV?;1g#VY`(&0rK7dN>2yun|<_Kc0cja)sf%DYE+$6cdOZ?)j*Xi6ID2 zTc0;>JVrxkZ<~5l)kJg%W&?YP@I`;Ifvs~%=0X&V8(51Y;A2Ih**!bqKelIX2y@F@ zZZ@vvDCNEw7K3dJDO*$!nB*_kUQS`SdI!ATEJ&w_hKCf1K;i*=U&k(g&$;iy4=I>% z1lt+>^$YSoCh?fq5c~c{Rq!#)&=F|XCa->Lye(((w;RCnS2w_tot0gD6TZ0{G6)?I zHW`=-bS75Xz4jc35UMK6FmT)?ssfqxAQCZth7_V1LAj{y;KPNDQ#92K)Bz(9N%!k` z2a#A)&lCro^CPx)M5RX-aH%KFkxS*kyJ_@$21A%aEk7}8CW$afdiA0q$o*Q8p~Hx$ z)wjiglw##_NXC6Vl=jhULoXf&pJ4Hm<6o(Z^CafzNerib zbnvubj_FjZSA0sYdT<0sXea}+%*Z(EdcuC6UesA;Y9VqK(g@4ang?%^8!$rctPQ6n z5i2knc#>$)2}_VLkvrMAA#=W`wgSz_vSbftwQyUey7{Zf2$|jF#34*$ucFZ{k#t@g ze6-)tkft1kOj>vg)WyW>!}-^fR&KnNh({BNC0-GpTngH1SP`~)2 zj7O6gm=;@eJsriqI23JPJgdcYtiAX!xO#>@&Ut-K@h|UjJR#XVBvE)|ggV%tW9e8mTtRM zFUiMa24>#+rnL$$x7!Bw^u;ND!gnDDOx|<5T`-99DwtZ`K!mB(++x$3gDZ5x0hl== zINQe3eJt(#?15iO9LO9y_;qsvB=V%dmb!{VAQDdF=D;S~(ROFhp3KL3kCJ2J8<&VmhsiQP$eL7U9k@r}tM15P9`RN&lXTw05C486XpKqDYkBstQ})mu4&C!DK)DWtUpzbX z@fX}cMz$j)JZWl2$f>ga8a1QF>;2_u+1pz4?2?yZuRwn`c4`kz$A3yTg!Gl-uuD(f z66uW$k*jzKJ8NJrf2-FAL}!OT0ErCrkvIp-w%Qgho+_Favi7HYKqQM&Zbt&BE(26Y zk4qY~;tiWZy#2^{AA&AX3{I7CRwCguacCg8N-P3k0`t7@mn9klQ%cCl>YTy-XsMye z@uYNy{U^#0*KFiN(+8q5C3j)g-vY8(H12jKB)kR%{lZx07zQ6y;9!YYy`#Xxc< zx*PA*yDQiZ56ui4gdv%|U2BBwK}F^`)uCK!B9hx3myVpcl{Ai(@Wjra`DN>NZQTA< zH!|-lxHn&2KU`}!hH%$TGSrckkC|+f>^s@Biun8ka3d6!xbR_<4e1f>8lMf*(sL@! za_gt7)}EcIT5Jp=mq9%G)C|2|oluD|^HbObHM90T#ZS@va1KO6S`6_BAv0vXPa z(6SaoxjESMl_cs@K#o&AnL-+dYw)+BGYV*_WmQ)A$O6+$@5Nl`+&0VY0Fg@qNznZmgE?rsR27%rK~pX_IA)Jxqx&Vtm1 z)av`JE1@$CGnkj2EW$Qy3^_86*UfO7L}0#3UDVYz%k)&L0Ic6Vukq$Gwu12qmw zl>|l_l|*KSlJnpnsTcIU92u!KxSl{H4#Ptyd7o*}xYZsOQPORH@ek$H<%psErs$1z z6ue%<)-Io=oTHJ%ZjMm%A!tj+&{_K2<)&P%^#HANIAoHM*t){fR*LQUYlS!=8CI`| zob>BT-wA29dr>6y%D~31gA#66JQq2eb@=7i{Ip&;twq2JPh)8lT01R{)b}xDg1f)M z9}3}bM6rcMhaQ}g-BGLXS^(0zzfmwC^9pP@spMaR#zP6Mi}9+P_fR65_i=SJ|BLte z(AN!!fzHSrvMjQW(Glj`T9W~*_T?d}#+lYuEXtl7O1{!*v*e%8FoQ#6<)eg?=JNd3 z>!U!+?l5nL3VbWM#h1Qek=Plt*yKfuL_z~CR04XqC>A7+$+A1NDMqdD z|9-7`efA`M@l-m)VF7TF2at)AT~n;*bgZqWvkq_MEOenG7nW04sj4^e4#ed)V#i`w_;&q}w^f~q$- zK8;+p>(n?{k&euw?P+;ynHs!8A4G@`cT&;6fgyuaAEKfzR-57_3$P+X-h0* zCeIZ6sDF-Nd zhRY?TJwrOP`P9wN@;vb;?n>%8PLWg*jTQn$hBOKc3U2B697vK1KaXfb3KpO|IizaC zoMBJNHCTieTNG4seV6Uv1gG_{flzFG1uHm_@pkifUE7qNmANdb_o%JoS3qc zO6y;=JVpMtX$NPAzTH1y@>NPb#pF8|Sw`x!v3^eaZ=c2V*Pn&!okuQKYRyLInj2ts zR)AH|j*VPwWRhLNq7_&_gmI`tl!|`=O=&Bu-k_5Rxcy3Ss5?97ti;YeD!XBZdzzKv zCXvUgpzX<4>aQO*{av=+`^&ObZlXCOF7ex$+S$)gSTWsrW@O)aNo;H~mY)2|)GYlk zFs2B8GtbZBnN@s`0UtOnDp{EQi^k~%m*g%GHi}4RJ`f^m683rr8lv7 zgoygt&aZjPr%I^$VPOD|pueNX`kL?@gXK-!{puiyIj}cRSoP5Wn(=^|rD$yhcF9=# z3L(wb-Yw^=9lf!Y3kz?0D~m_qP4aw6c{2M!_u}=~mayipZZ&62I?qSH-Efuv-8nA( z>&`JYu^%5uLbkTEN+`D8-n8j#x(_1)ws!t*_?K{ z8dZ|83U5G1~H_yY1)4i5Gy?WZyy^fD_reBpFK5HiLuYRvU+HPhR zd$-m|e8?3n0gIGuzxx+teCb~_`0uav3mXoV-e%XUlgMhk^~2q=aXJ;hx|bhUaBQu+ zx4^ZRpAap~<2hsO$>|_@LriySkC;1y-L;~lcd$3XZNka?{{2zQ`kh<@@-`>)ofm5l z8Jhu;s9p43G6?E8MweyK`19BN95qYt64Q$OkMLSQlyx_D=r@8uG}&UZdYw+h! zTa(70PuCvez=!&LBjRtIe!9lp7trUlbU$B88Bd?3O@E0rwAH2`d{oiO(Qof@i0K{9 zoR*9d=RN)FUBGqsRgt<%z(+Y1tx0BTC>av498C*9RD;a3SIqat}E8tTnwcmt|uc*H{)Oy{%B<(;)zhM`KG47<|SVcEg2Rb_iI ze^pgBW;5D|kR3g35#w&E~Z4KM^&rWKO@ z=()4qTF}UJ9Aqx_{&eu6y*LEpwII1(7%cJVVC?7^U;+La05Z2g8O=;CIg^iu!!ddZ zg4rejh>8!~UoVk8h8XC5VPmwB0&A}RcE|ZfN{ArKTn2e~6fzcT0kptYAZt$AjKi1& zGXOCDE)GJ_7cpx{kG%J5zJ|dO(~d?7jpV+!4og>z0q0x8iJ-ZuBKK+z)Y=x zx01Z$W=AW*`>D?su0Yj_pkiO`FjR?|PgVI(1g{nXu`!~};TL%N=?cI-u0-J7zK-y= zv=EgYi*0kzdn1TSxfvv000;dS-kx&B(4d%NfroH+g)C~ z5iFH;c(4?lba=3qcSHuUVK;rD?Q4O+J!Yh9n>oTJWJa<2j)yLT>^B-5^d}+7jXShC zbUvIyCZ^>_3}+${Xr;BN8Dw|fXkoO+NG7ombR8#8P>&U9$gZb(dH}DxpArLu|3XRYSq)7a(p!tf*jMM!IYOFfwu2c}ceK6G^j!&DY22UiwdmW4PDHDdx(L0u0KC`= zN}OY`)JEvsfIoR|O%uvT66k?UY_kH0iUMwlZAFb}-hiaCsy$~fD3@j&f|7U&8N>4Q z{E!yL4RAG8jt{1|J3;^|s5k=`&5Jft`UI8Elk+#5GzR?GEyYcF{?^gh26DB#RKATQ;2Oi9aC2db*K12%cxV z{Tx#_{`qFBb4v0IpLGx#LY9jasDk3dk4;XRjO|{{fHcC^zTm?(Hz@0sggs!kR}|=4 zWjj1!7+Mrm5j;UqVj4svjBAi)5G%aThH93(q0MoQqz^A-obd&;z$Tb1|A{x;)DG+o zKr3Sg(!b%#d_?v3QOXS6yyV3ml5u=D?`S6vnHJYR`0gof!puxxG{dzF4guhCCP3^T zr%&D1PZ7M`%JX7(cLxZJDW0?@O8I8?vB7)9g;Q{Sp6dyr!64pGGDKVj?vhT?EI!*; zE~75#`Da|&T{=2k63=OqybpVU)YW`s+BlC=*DZ@i7jQwl$B|%v(km~ujEJSx0&mcELHOl!vPz&w#AN>7_KQ;qo#qI}Nd%Gj;nTL3WPEAc;1bp@ZekgLV z44I@7+bLLhmqq%e(65CN|BMom%ZK2S8AA{`Px%{BOWrFi$~2!9l*f%t1c9B@<>Z$@ z;Z+lVHboK_-QO?GPsb1naa?}*q?BmmDe7sac|q0_040_Cn~iCl_j-s^X;&3A^zuvH z;11)lT8EEqO`8ORmY&Lb4g4!fG`#yZjMqRB9p`?QrtMzOkO1qMA4y6WGJ(V9pAi9L z_eyO?dI=BP>~hNsN;mrZrDH!-6j&za$wz6uArl8Y&z?Dg=g4zKEo)TUi-Gf98))LPzL1h?f z$kzCuX$$^t8d~mEwRA@+R#m&WNc`m9Q{FO0XM3Ap;1W0zFh0aFq-e@lR}ODpMBp$} z#U{(k%H!#;_wBL^9X@j|mt|cEIBU4#nB#ro-cL#n1P^0gH~QWcGPF~7Mn*)r`3uEo z)N=Q!ou41Y^l1rPmGFaVDnsZmLmSH)svu3))T=8xgs?Ex#&Y_R&kV_W2Kg({E$JQ) zwwsDxYM6vj4B#u_geA-p1k4_I=F;L&d^TYhKoNe&V$1U8A1C=~D)6)Y7aWGM&)Rw| z`@Y=yCNXi7h^CEr-ofyG=#1PQ4lhH(R>|XdsuqnL*(fiFExFZpcY}Alg-TwdN@qZB znLVv3XDnQ>Y8(x%Gfn%Nu(t{ZOMFO_)V_ycH#QS-W5&NT%)Z?b)xs6x*=1zLAtKHF z<-jV7e~H$7^%Ef-uF-URNN-bTJJtqFL})*U-P?y^fdgT{H1*|YFW#3Y%;OWujF1R2yWdSE68=q`fSsbXHWI-2<6}|POCs$YIXASgyM|yS zp(2-LhiKHjB~G7+6T0kr83B$MaOXXG;YBJEGd55DqWLNY{hB{5Hk1_Vjn%k%qA80! z_l~7J-+gnP7i@RoYTamA7BX9O&s7vzNk;RsS>R%jEgMVbyM_oBlOjH=ViF$GeK&ML zK+O2=YPE+KJL~&x_$?d!UqV5(zcnbx&ie<2d-o82l0@x0s=fe`CdH)a=$yxZ6kMkN z2vY3U+IXJW)EswL@Jah_ZLfKC%+sBSer#{Ll`#69>b0D$iMw0EJN`EgkCh)qzq}*b z+EjpGD}S7k2gB7b8jXayIrAzeEUSntyiepWx2Gmbdgh7nMH*CkJ$T#X@CC6h|NXMy z3&@bDIar@3n8Y46@j7`~A+nbrA+hiI{+Yu?e$4sA`Ttx(qiOi6y%X>#ql;}mHqIEe zmarHRMc)=i8`gI0&wtV{I&#~RX$Ii$@;fn$9YaW-KA@y1*4(Av@~(dc2mV{>^N$L+ zfA@|5lYQgA^Z87*K@l(qaC z*Q~Gci}i9nj{=eS|vc_wInIeW|c5 zDv)4xXP7b+DxWYPf=f<>KnfVn9eR8z%fYr~ICvvrzpd{?Ot2KPM>~D3h?kEWG-z(X z%}429HR1weA-)xO8jpd(t2JcN=j%AMn@KIv^GH9+CS68KHIIUTD^^ zRU{*Ku(|NMKL&RNOUIuu{BCrqOa-)H7Yc2l2_prAvvq|ZJ1dG!Pbm%JsSq!=N`oS~ z#7Svzwjyu@1(Ji0^jNJLXM5MV&iluclVMb_pLA5)x152+w?b&_F$_=c^#CE3S8 zC6eI=KD9`MRfO5p5>`+*-nLj<1C#MNgm|8K{QMgUXvQ2Wx&oH`$4HULa(ZEFCFd>Z zOvymwXj#!{3J(%#%t(+j)X=%Q-I{D&4L@p&86bpe98YLVRfJo%h)zy^1@NR5%*fQi zM>$IYr?R0pQM*!rs6mO>_0N}M)S&o(CbljyT;-HJQ~y0hTpOXvSEvr4NTy1J0DvZH z5*%CTK*?co$+m#O8uf?+uG)KlT^5?)W1vD|@j$?BSUD0%T-Zvg4+7#qp!s>c&sqYI zF<#BsvK4Rv8uX6tH>~~(lqI?-HN?6)1ex=#IHUq7`1L#lAL@Lzd=t=s+hI0vac2n*0IRSyb z{##!e<~n5Yu>S;NeP|R3y^AEk6{O2srz9W?LPoq(W0|()UzKEZU#ZPI5NHHm^a`2? zy!G~rWzPZtMO(42D9QJ$M#9LD;5%d_cIZ}(n>QkHp@~N%gK^~L$Eh?$br4xoE11$z z0B$uJM(#jM)(T+6@yZu-<%Y@yT1uTT+kgST3sDw5;zf+&`z$MH6+GrFUhanm!1sgR zFeS#dVIBdtGJI@zs~-;r?^~Re7%3z|9_4A0(MoPi=4^UXdoH)XR+Z>$OJQCZbH!!~ z1|$=DXn-c4;F2n59(PnC)JlFUK6Tr|qjqbZB=r1zUDV%Ph@De~zN^d{tLli|56OSNTSLI6IY8N+K&}P@LhKo}kuXtlAKL>9I zZ6v`G7q=3rlQC@ix#9_}S(Fd2Iov&@ytMwtdJvXc-TW*b$-Z zzvV7GUNW+;ffzHig^DX>5X*pVcRjKU{wz{|5rY+PU}Ue*u8P&t*1Z!SEu+&W)IurT^TGZj5{-PhcDj*Z>z8GNL*gZdK%Twow4uEdE#mg|S{J z986dJy+wd-3sKLfqvX8MzMHV36P&L3`bEo!b>1rrSob#a@*;+6w7{c-k^c-Uzdaix zAKL>&pE13+i;J8EO_y&ln2Yr7Jt*!WOjcKIXJ6X3&Ol17b<>7^%M0 zp1l>noyX4~6AEPp&pjVJGvH(8+HR-1D)8agyn-S7^}RjNu`^AQCHArlw^hDqpk=p| zdCIQFV75^8q50#vg<~zO+^6(LcJ3R@R8c4CpX0_y=*U{31i9f4MvK0 z{>MTJi*^e{#b;aHP-fleXt`(o=|zjV4}{-y0SRSJXI;8@H*iU+z~N>WwhP+tF4e5E z*cr>Q_VY_oGq+@^)CJ3VbgXtU)I9Yqcx0rmRXjmy&Z9j`8Y@^;K;C`msKoq2v^>); zbj}^U)?;?EZ#H?QIsRYs@#XtXKdzFxSa`va zy_K2ILIJctDDlaOiKWIDE~>g8sPW<}43(XsAG&n&j0CB;UMW*8(DtmOKTOT~LX@qp z{M@jS@$`FHJB}Itht;p&?YL6&lC3Lmu`r{WB51K>nE%o*Qv{ zV}^dS2s>aMLE0L%BrOgVyYFx51!0inEfo6d-Al4sh?Gqe@rcV z+q$Gx{aXE(R`nIB?`HbzdPe}y6q$a8bH#@0?!H5lqmm_86`WXIcD`nn)!tbCb2Trm zmgE_<1Sqdx6?yrh!IovL=Kofo?gu*3o;~-|ftt+VZMPn0`vP0n8T{|xWU{ej{YpO1 zaDT(=OEr>Zn(~txvLA(-E;_vWczet5!^?y}XWm${OS^HFer)5#-Y-rEE^D&}AL8E0 z<>$v8Ydk+{#+Dwv+4_tKPd0$s7ZLX_YDl4-h^PsP_!1~+XpI5&7Q)X@TdFkzrUA6)% zI>WE(sN}AQ3@-Zfe40GtXU%%PTTdE)URsv3_4xH!6`{Q^KD_$)?GNMDgTbFy@|SaJgxA znL^z5>Cb}i{(M_j^2~d?U}?2NfG{|m12!*v^5eX&JrlBR{p-OA_~F&ZZ+|jpH7|b_ zEWIhDum731<<1o+Crd4VyGl?t?nLP2`BDEO&Mg09nK7CDnfL5XSNY=iPT2Cn=4b!D z4=h{H*nVD_%6aO01riRev+g~4?sUO>Zh4G0q!lf(AjW+$@J-ojVUe%YEE&~aql*Z2PS^89WlW87nU{E-I00lzSCE|S^ig- z?^w+U%r32$d;iw6WIYNkU9?!u^~L@jSp4&!T|0HbrnNu!%x3@sPgg&ebxsLQ05$M@ As{jB1 literal 0 HcmV?d00001 diff --git a/site/content/docs/1.29/img/source/shutdownmanager.drawio b/site/content/docs/1.29/img/source/shutdownmanager.drawio new file mode 100644 index 00000000000..99b86620c42 --- /dev/null +++ b/site/content/docs/1.29/img/source/shutdownmanager.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/site/content/docs/1.29/redeploy-envoy.md b/site/content/docs/1.29/redeploy-envoy.md new file mode 100644 index 00000000000..2456b53d2bf --- /dev/null +++ b/site/content/docs/1.29/redeploy-envoy.md @@ -0,0 +1,72 @@ +# Redeploying Envoy + +The Envoy process, the data path component of Contour, at times needs to be re-deployed. +This could be due to an upgrade, a change in configuration, or a node-failure forcing a redeployment. + +When implementing this roll out, the following steps should be taken: + +1. Stop Envoy from accepting new connections +2. Start draining existing connections in Envoy by sending a `POST` request to `/healthcheck/fail` endpoint +3. Wait for connections to drain before allowing Kubernetes to `SIGTERM` the pod + +## Overview + +Contour implements an `envoy` sub-command named `shutdown-manager` whose job is to manage a single Envoy instances lifecycle for Kubernetes. +The `shutdown-manager` runs as a new container alongside the Envoy container in the same pod. +It uses a Kubernetes `preStop` event hook to keep the Envoy container running while waiting for connections to drain. The `/shutdown` endpoint blocks until the connections are drained. + +```yaml + - name: shutdown-manager + command: + - /bin/contour + args: + - envoy + - shutdown-manager + image: ghcr.io/projectcontour/contour:main + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: + - /bin/contour + - envoy + - shutdown +``` + +The Envoy container also has some configuration to implement the shutdown manager. +First the `preStop` hook is configured to use the `/shutdown` endpoint which blocks the Envoy container from exiting. +Finally, the pod's `terminationGracePeriodSeconds` is customized to extend the time in which Kubernetes will allow the pod to be in the `Terminating` state. +The termination grace period defines an upper bound for long-lived sessions. +If during shutdown, the connections aren't drained to the configured amount, the `terminationGracePeriodSeconds` will send a `SIGTERM` to the pod killing it. + +![shutdown-manager overview][1] + +### Shutdown Manager Config Options + +The `shutdown-manager` runs as another container in the Envoy pod. +When the pod is requested to terminate, the `preStop` hook on the `shutdown-manager` executes the `contour envoy shutdown` command initiating the shutdown sequence. + +The shutdown manager has a single argument that can be passed to change how it behaves: + +| Name | Type | Default | Description | +|------------|------|---------|-------------| +| serve-port | integer | 8090 | Port to serve the http server on | +| ready-file | string | /admin/ok | File to poll while waiting shutdown to be completed. | + +### Shutdown Config Options + +The `shutdown` command does the work of draining connections from Envoy and polling for open connections. + +The shutdown command has a few arguments that can be passed to change how it behaves: + +| Name | Type | Default | Description | +|------------|------|---------|-------------| +| check-interval | duration | 5s | Time interval to poll Envoy for open connections. | +| check-delay | duration | 0s | Time wait before polling Envoy for open connections. | +| drain-delay | duration | 0s | Time wait before draining Envoy connections. | +| min-open-connections | integer | 0 | Min number of open connections when polling Envoy. | +| admin-port (Deprecated) | integer | 9001 | Deprecated: No longer used, Envoy admin interface runs as a unix socket. | +| admin-address | string | /admin/admin.sock | Path to Envoy admin unix domain socket. | +| ready-file | string | /admin/ok | File to write when shutdown is completed. | + + [1]: ../img/shutdownmanager.png diff --git a/site/content/docs/1.29/start-contributing.md b/site/content/docs/1.29/start-contributing.md new file mode 100644 index 00000000000..2ddefb6c485 --- /dev/null +++ b/site/content/docs/1.29/start-contributing.md @@ -0,0 +1,130 @@ +# Getting Started with Contributing + +Thanks for your interest in contributing to Contour. Community contributions are always needed, welcome, and appreciated. This guide shows how you can contribute to Contour in the following areas: + +- Code +- Website +- Documentation + +Please familiarize yourself with the [Code of Conduct][1] and project [Philosophy][15] before contributing. + +# Getting Started with Code + +Everything is managed on the [Project Contour GitHub][2] organization. Create an issue for a new idea or look for issues labeled **good first issue** to get started. + +## How we work + +See [How We Work][3] for an overview: +- Issue management +- Code reviews +- Coding practice +- GitHub labels + +## Contribution workflow + +Review the [Contribution workflow][4] to understand how to work with the code. + +Below is a list of workflow areas: +- Building from source +- Contribution workflow +- Contour testing +- Developer Certificate of Origin (DCO) sign off + +# Getting Started with the Website + +Updates, corrections, or improvements are managed through [GitHub][16] issues. + +When you are ready to take on an issue, see [Website Contribution Guidelines][5] to understand how the Contour website contributions are managed. There is information on: +- Site structure +- Link formatting +- Testing +- Setting up your environment + +# Getting Started with Documentation + +Documentation is critical to the success of any project. Open to all levels, Contour needs help to create and update its documentation. Join the [Contour Community Meetings][8] meeting and learn more about the Tech Docs Working Group. + +Review the [Contour Technical Documentation Contributing Guide][6] for instructions to set up your environment. + +Technical documentation will follow the [Website Contribution Guidelines][5]. + +## New documentation suggestions + +If you have a document suggestion, create an issue in [GitHub][16]. The team will triage and prioritize the issue. Connect on Slack or in a meeting to discuss your issue or request. + +## Helping with identified document issues + +Take a look at the project issues list with the label **area/documentation**. If you are new to technical writing, add in the **good first issue** label: +[area/documentation and good first issue][7] + +Reach out on Slack or a Contour meeting for any assistance. Help is always appreciated. + +# Filing and Working on Issues + +Whether code, website, or documentation, Contour uses GitHub to create, track, and manage all issues. + +If there is a fix or a suggestion for improvement, create an issue in [GitHub][16]. + +All issues are reviewed and evaluated by the Contour team. + +# Meet the Community and the Team + +To find out more about contributing to Contour, connect with us at a Contour Community Meeting, on Slack, or through the mailing list. We also have an Office Hours meeting to answer “How do I…” questions. + +## Contour Community meetings + +Discuss issues, features, or suggestions with the Contour team and other community members. Ask anything and find out more about Contour. + +Ask questions: +- “How do I do this in Contour?” +- “Why does Contour do this thing this way?” +- “Where can I find…?” + +See the [Community][8] page for: +- Meeting schedule +- Meeting notes with zoom link +- Meeting recordings + +## Mailing list + +To get email updates to Contour, join the [mailing list][10]. Topics include: +- Release notifications +- Issues +- Feedback and suggestions +- Meeting notifications + +## Find us +There are many ways to connect with the Contour team: + +- Slack: Kubernetes [#contour][11] +- Contour YouTube Channel: [CNCF Contour][12] +- Twitter: [@projectcontour][13] +- GitHub: [projectcontour][14] + +# Want More Contributing Information? + +Slack or a meeting is a great way to introduce yourself. Let us know what you are interested in, your background, and what you want to accomplish. + +# Next steps + +Come out and join a [Community meeting][8] or an [Office Hours meeting][9]. Ask questions about how to get started or just sit back and get to know the team. + + + +[1]: {{< param github_url >}}/blob/main/CODE_OF_CONDUCT.md +[2]: https://github.com/projectcontour +[3]: {{< relref "resources/how-we-work.md" >}} +[4]: {{< param github_url >}}/blob/main/CONTRIBUTING.md +[5]: {{< param github_url >}}/blob/main/SITE_CONTRIBUTION.md +[6]: {{< relref "resources/contributing-docs.md" >}} +[7]: {{< param github_url >}}/issues/?q=is%3Aopen+is%3Aissue+label%3Aarea%2Fdocumentation+label%3A%22good+first+issue%22 +[8]: {{< relref "community.md" >}} +[9]: https://github.com/projectcontour/community/wiki/Office-Hours +[10]: https://lists.cncf.io/g/cncf-contour-users/ +[11]: {{< param slack_url >}} +[12]: https://www.youtube.com/channel/UCCde7QSfcyYJ8AuXofD5bTA +[13]: https://twitter.com/projectcontour +[14]: https://github.com/projectcontour +[15]: {{< relref "resources/philosophy.md" >}} +[16]: {{< param github_url >}}/issues/ +[17]: {{< param github_url >}}/ \ No newline at end of file diff --git a/site/content/docs/1.29/troubleshooting.md b/site/content/docs/1.29/troubleshooting.md new file mode 100644 index 00000000000..28461bd8641 --- /dev/null +++ b/site/content/docs/1.29/troubleshooting.md @@ -0,0 +1,41 @@ +## Troubleshooting + +If you encounter issues, follow the guides below for help. For topics not covered here, you can [file an issue][0], or talk to us on the [#contour channel][1] on Kubernetes Slack. + +### [Troubleshooting Common Proxy Errors][2] +A guide on how to investigate common errors with Contour and Envoy. + +### [Envoy Administration Access][3] +Review the linked steps to learn how to access the administration interface for your Envoy instance. + +### [Contour Debug Logging][4] +Learn how to enable debug logging to diagnose issues between Contour and the Kubernetes API. + +### [Envoy Debug Logging][5] +Learn how to enable debug logging to diagnose TLS connection issues. + +### [Visualize the Contour Graph][6] +Learn how to visualize Contour's internal object graph in [DOT][9] format, or as a png file. + +### [Show Contour xDS Resources][7] +Review the linked steps to view the [xDS][10] resource data exchanged by Contour and Envoy. + +### [Profiling Contour][8] +Learn how to profile Contour by using [net/http/pprof][11] handlers. + +### [Envoy container stuck in unready/draining state][12] +Read the linked document if you have Envoy containers stuck in an unready/draining state. + +[0]: {{< param github_url >}}/issues +[1]: {{< param slack_url >}} +[2]: /docs/{{< param version >}}/troubleshooting/common-proxy-errors/ +[3]: /docs/{{< param version >}}/troubleshooting/envoy-admin-interface/ +[4]: /docs/{{< param version >}}/troubleshooting/contour-debug-log/ +[5]: /docs/{{< param version >}}/troubleshooting/envoy-debug-log/ +[6]: /docs/{{< param version >}}/troubleshooting/contour-graph/ +[7]: /docs/{{< param version >}}/troubleshooting/contour-xds-resources/ +[8]: /docs/{{< param version >}}/troubleshooting/profiling-contour/ +[9]: https://en.wikipedia.org/wiki/Dot +[10]: https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol +[11]: https://golang.org/pkg/net/http/pprof/ +[12]: /docs/{{< param version >}}/troubleshooting/envoy-container-draining/ diff --git a/site/content/docs/1.29/troubleshooting/common-proxy-errors.md b/site/content/docs/1.29/troubleshooting/common-proxy-errors.md new file mode 100644 index 00000000000..e05f153242d --- /dev/null +++ b/site/content/docs/1.29/troubleshooting/common-proxy-errors.md @@ -0,0 +1,96 @@ +# Troubleshooting Common Proxy Errors + +## Unexpected HTTP errors + +Here are some steps to take in investigating common HTTP errors that users may encounter. +We'll include example error cases to debug with these steps. + +1. Inspect the HTTP response in detail (possibly via `curl -v`). + + Here we're looking to validate if the error response is coming from the backend app, Envoy, or possibly another proxy in front of Envoy. + If the response has the `server: envoy` header set, the request at least made it to the Envoy proxy so we can likely rule out anything before it. + The error may originate from Envoy itself or the backend app. + Look for headers or a response body that may originate from the backend app to verify if the error is in fact just the intended app behavior. + In the example below, we can see the response looks like it originates from Envoy, based on the `server: envoy` header and response body string. + + ``` + curl -vvv example.projectcontour.io + ... + > GET / HTTP/1.1 + > Host: example.projectcontour.io + ... + > + < HTTP/1.1 503 Service Unavailable + < content-length: 91 + < content-type: text/plain + < vary: Accept-Encoding + < date: Tue, 06 Feb 2024 03:44:30 GMT + < server: envoy + < + * Connection #0 to host example.projectcontour.io left intact + upstream connect error or disconnect/reset before headers. reset reason: connection failure + ``` + +1. Look at the Envoy pod logs for the access logs corresponding to the erroring request/response. + + The exact fields/field ordering present in the access log may vary if you have [configured a custom access log string or JSON access logs][0]. + For example for a Contour installation using the [default Envoy access log format][1] we would want to inspect: + * `%REQ(:METHOD)%`, `%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%`, `%REQ(:AUTHORITY)%`, `%PROTOCOL%`: Ensure these are sensible values based on your configured route and HTTP request + * `%RESPONSE_FLAGS%`: See the [documentation on Envoy response flags][2] and below how to interpret a few of them in a Contour context: + * `UF`: Likely that Envoy could not connect to the upstream + * `UH`: Upstream Service has no health/ready pods + * `NR`: No configured route matching the request + * `%DURATION%`: Can correlate this with any configured timeouts + * `%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%`: Can correlate this with any configured timeouts. If `-` then this is a hint the request was never forwarded to the upstream. + * `%UPSTREAM_HOST%`: This is the IP of the upstream pod that was selected to proxy the request to and can be used to verify the exact upstream instance that might be erroring. + + For example in this access log: + + ``` + [2024-02-06T15:18:17.437Z] "GET / HTTP/1.1" 503 UF 0 91 1998 - "103.67.2.26" "curl/8.4.0" "d70640ec-2feb-46f8-9f63-24c44142c42e" "example.projectcontour.io" "10.244.8.27:8080" + ``` + + We can see the `UF` response flag as the cause of the `503` response code. + We also see the `-` for upstream request time. + It is likely in this case that Envoy was not able to establish a connection to the upstream. + That is further supported by the request duration of `1998` which is approximately the default upstream connection timeout of `2s`. + +1. Inspect Envoy metrics + + This method of debugging can be useful especially for deployments that service a large volume of traffic. + In this case, access logs are possibly not suitable to use, as the volume of logs may be too large to pinpoint an exact erroring request. + + Metrics from individual Envoy instances can be viewed manually or scraped using Envoy's prometheus endpoints and graphed using common visualization tools. + See the `/stats/prometheus` endpoint of the [Envoy admin interface][3]. + + Metrics that may be useful to inspect: + * [Listener metrics][4] + * `downstream_cx_total` + * `ssl.connection_error` + * [HTTP metrics][5] + * `downstream_cx_total` + * `downstream_cx_protocol_error` + * `downstream_rq_total` + * `downstream_rq_rx_reset` + * `downstream_rq_tx_reset` + * `downstream_rq_timeout` + * `downstream_rq_5xx` (and other status code groups) + * [Upstream metrics][6] + * `upstream_cx_total` + * `upstream_cx_connect_fail` + * `upstream_cx_connect_timeout` + * `upstream_rq_total` + * `upstream_rq_timeout` + +1. Send a direct request to the backend app to narrow down where the error may be originating. + + This can be done via a port-forward to send a request to the app directly, skipping over the Envoy proxy. + If this sort of request succeeds, we know the issue likely originates from Contour configuration or the Envoy proxy rather than the app itself. + +[0]: /docs/{{< param latest_version >}}/config/access-logging/ +[1]: https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#default-format-string +[2]: https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-response-flags +[3]: /docs/{{< param latest_version >}}/guides/prometheus/#envoy-metrics +[4]: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/stats +[5]: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/stats +[6]: https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats diff --git a/site/content/docs/1.29/troubleshooting/contour-debug-log.md b/site/content/docs/1.29/troubleshooting/contour-debug-log.md new file mode 100644 index 00000000000..821634242c6 --- /dev/null +++ b/site/content/docs/1.29/troubleshooting/contour-debug-log.md @@ -0,0 +1,6 @@ +# Enabling Contour Debug Logging + +The `contour serve` subcommand has two command-line flags that can be helpful for debugging. +The `--debug` flag enables general Contour debug logging, which logs more information about how Contour is processing API resources. +The `--kubernetes-debug` flag enables verbose logging in the Kubernetes client API, which can help debug interactions between Contour and the Kubernetes API server. +This flag requires an integer log level argument, where higher number indicates more detailed logging. diff --git a/site/content/docs/1.29/troubleshooting/contour-graph.md b/site/content/docs/1.29/troubleshooting/contour-graph.md new file mode 100644 index 00000000000..5abcfeb22af --- /dev/null +++ b/site/content/docs/1.29/troubleshooting/contour-graph.md @@ -0,0 +1,25 @@ +# Visualizing Contour's Internal Object Graph + +Contour models its configuration using a directed acyclic graph (DAG) of internal objects. +This can be visualized through a debug endpoint that outputs the DAG in [DOT][2] format. +To visualize the graph, you must have [`graphviz`][3] installed on your system. + +To download the graph and save it as a PNG: + +```bash +# Port forward into the contour pod +$ CONTOUR_POD=$(kubectl -n projectcontour get pod -l app=contour -o name | head -1) +# Do the port forward to that pod +$ kubectl -n projectcontour port-forward $CONTOUR_POD 6060 +# Download and store the DAG in png format +$ curl localhost:6060/debug/dag | dot -T png > contour-dag.png +``` + +The following is an example of a DAG that maps `http://kuard.local:80/` to the +`kuard` service in the `default` namespace: + +![Sample DAG][4] + +[2]: https://en.wikipedia.org/wiki/DOT +[3]: https://graphviz.gitlab.io/ +[4]: /img/kuard-dag.png diff --git a/site/content/docs/1.29/troubleshooting/contour-xds-resources.md b/site/content/docs/1.29/troubleshooting/contour-xds-resources.md new file mode 100644 index 00000000000..69f413a8cb3 --- /dev/null +++ b/site/content/docs/1.29/troubleshooting/contour-xds-resources.md @@ -0,0 +1,19 @@ +# Interrogate Contour's xDS Resources + +Sometimes it's helpful to be able to interrogate Contour to find out exactly what [xDS][1] resource data it is sending to Envoy. +Contour ships with a `contour cli` subcommand which can be used for this purpose. + +Because Contour secures its communications with Envoy using Secrets in the cluster, the easiest way is to run `contour cli` commands _inside_ the pod. +Do this is via `kubectl exec`: + +```bash +# Get one of the pods that matches the examples/daemonset +$ CONTOUR_POD=$(kubectl -n projectcontour get pod -l app=contour -o jsonpath='{.items[0].metadata.name}') +# Do the port forward to that pod +$ kubectl -n projectcontour exec $CONTOUR_POD -c contour -- contour cli lds --cafile=/certs/ca.crt --cert-file=/certs/tls.crt --key-file=/certs/tls.key +``` + +Which will stream changes to the LDS api endpoint to your terminal. +Replace `contour cli lds` with `contour cli rds` for route resources, `contour cli cds` for cluster resources, and `contour cli eds` for endpoints. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol diff --git a/site/content/docs/1.29/troubleshooting/envoy-admin-interface.md b/site/content/docs/1.29/troubleshooting/envoy-admin-interface.md new file mode 100644 index 00000000000..c44b6fe3e9d --- /dev/null +++ b/site/content/docs/1.29/troubleshooting/envoy-admin-interface.md @@ -0,0 +1,32 @@ +# Accessing the Envoy Administration Interface + +Getting access to the Envoy [administration interface][1] can be useful for diagnosing issues with routing or cluster health. +However, Contour doesn't expose the entire Envoy Administration interface since that interface contains many options, such as shutting down Envoy or draining traffic. +To prohibit this behavior, Contour only exposes the read-only options from the admin interface which still allows for debugging Envoy, but without the options mentioned previously. + +Those endpoints are: + - /certs + - /clusters + - /listeners + - /config_dump + - /memory + - /ready + - /runtime + - /server_info + - /stats + - /stats/prometheus + - /stats/recentlookups + +The Envoy administration interface is bound by default to `http://127.0.0.1:9001`. +To access it from your workstation use `kubectl port-forward` like so: + +```sh +# Get one of the pods that matches the Envoy daemonset +ENVOY_POD=$(kubectl -n projectcontour get pod -l app=envoy -o name | head -1) +# Do the port forward to that pod +kubectl -n projectcontour port-forward $ENVOY_POD 9001 +``` + +Then navigate to `http://127.0.0.1:9001/` to access the administration interface for the Envoy container running on that pod. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/operations/admin diff --git a/site/content/docs/1.29/troubleshooting/envoy-container-draining.md b/site/content/docs/1.29/troubleshooting/envoy-container-draining.md new file mode 100644 index 00000000000..82bb47cb883 --- /dev/null +++ b/site/content/docs/1.29/troubleshooting/envoy-container-draining.md @@ -0,0 +1,29 @@ +# Envoy container stuck in unready/draining state + +It's possible for the Envoy containers to become stuck in an unready/draining state. +This is an unintended side effect of the shutdown-manager sidecar container being restarted by the kubelet. +For more details on exactly how this happens, see [this issue][1]. + +If you observe Envoy containers in this state, you should `kubectl delete` them to allow new Pods to be created to replace them. + +To make this issue less likely to occur, you should: +- ensure you have [resource requests][2] on all your containers +- ensure you do **not** have a liveness probe on the shutdown-manager sidecar container in the envoy daemonset (this was removed from the example YAML in Contour 1.24.0). + +If the above are not sufficient for preventing the issue, you may also add a liveness probe to the envoy container itself, like the following: + +```yaml +livenessProbe: + httpGet: + path: /ready + port: 8002 + initialDelaySeconds: 15 + periodSeconds: 5 + failureThreshold: 6 +``` + +This will cause the kubelet to restart the envoy container if it does get stuck in this state, resulting in a return to normal operations load balancing traffic. +Note that in this case, it's possible that a graceful drain of connections may or may not occur, depending on the exact sequence of operations that preceded the envoy container failing the liveness probe. + +[1]: https://github.com/projectcontour/contour/issues/4851 +[2]: /docs/{{< param latest_version >}}/deploy-options/#setting-resource-requests-and-limits \ No newline at end of file diff --git a/site/content/docs/1.29/troubleshooting/envoy-debug-log.md b/site/content/docs/1.29/troubleshooting/envoy-debug-log.md new file mode 100644 index 00000000000..bfef4fa5531 --- /dev/null +++ b/site/content/docs/1.29/troubleshooting/envoy-debug-log.md @@ -0,0 +1,8 @@ +# Enabling Envoy Debug Logging + +The `envoy` command has a `--log-level` [flag][1] that can be useful for debugging. +By default, it's set to `info`. +To change it to `debug`, edit the `envoy` DaemonSet in the `projectcontour` namespace and replace the `--log-level info` flag with `--log-level debug`. +Setting the Envoy log level to `debug` can be particilarly useful for debugging TLS connection failures. + +[1]: https://www.envoyproxy.io/docs/envoy/latest/operations/cli diff --git a/site/content/docs/1.29/troubleshooting/profiling-contour.md b/site/content/docs/1.29/troubleshooting/profiling-contour.md new file mode 100644 index 00000000000..95bb0164210 --- /dev/null +++ b/site/content/docs/1.29/troubleshooting/profiling-contour.md @@ -0,0 +1,14 @@ +# Accessing Contour's /debug/pprof Service + +Contour exposes the [net/http/pprof][1] handlers for `go tool pprof` and `go tool trace` by default on `127.0.0.1:6060`. +This service is useful for profiling Contour. +To access it from your workstation use `kubectl port-forward` like so, + +```bash +# Get one of the pods that matches the Contour deployment +$ CONTOUR_POD=$(kubectl -n projectcontour get pod -l app=contour -o name | head -1) +# Do the port forward to that pod +$ kubectl -n projectcontour port-forward $CONTOUR_POD 6060 +``` + +[1]: https://golang.org/pkg/net/http/pprof diff --git a/site/content/resources/compatibility-matrix.md b/site/content/resources/compatibility-matrix.md index a4326642408..f2d57fd78d1 100644 --- a/site/content/resources/compatibility-matrix.md +++ b/site/content/resources/compatibility-matrix.md @@ -11,6 +11,7 @@ These combinations of versions are specifically tested in CI and supported by th | Contour Version | Envoy Version | Kubernetes Versions | Gateway API Version | | --------------- | :------------------- | ------------------- | --------------------| | main | [1.30.1][53] | 1.29, 1.28, 1.27 | [1.0.0][110] | +| 1.29.0 | [1.30.1][53] | 1.29, 1.28, 1.27 | [1.0.0][110] | | 1.28.4 | [1.29.4][55] | 1.29, 1.28, 1.27 | [1.0.0][110] | | 1.28.3 | [1.29.3][50] | 1.29, 1.28, 1.27 | [1.0.0][110] | | 1.28.2 | [1.29.2][49] | 1.29, 1.28, 1.27 | [1.0.0][110] | diff --git a/site/data/docs/1-29-toc.yml b/site/data/docs/1-29-toc.yml new file mode 100644 index 00000000000..151db52d41a --- /dev/null +++ b/site/data/docs/1-29-toc.yml @@ -0,0 +1,151 @@ +toc: + - title: Introduction + subfolderitems: + - page: Contour Architecture + url: /architecture + - page: Contour Philosophy + link: /resources/philosophy + - title: Configuration + subfolderitems: + - page: HTTPProxy Fundamentals + url: /config/fundamentals + - page: Gateway API Support + url: /config/gateway-api + - page: Ingress v1 Support + url: /config/ingress + - page: Virtual Hosts + url: /config/virtual-hosts + - page: Inclusion and Delegation + url: /config/inclusion-delegation + - page: TLS Termination + url: /config/tls-termination + - page: Upstream TLS + url: /config/upstream-tls + - page: Request Routing + url: /config/request-routing + - page: External Service Routing + url: /config/external-service-routing + - page: Request Rewriting + url: /config/request-rewriting + - page: CORS + url: /config/cors + - page: Websockets + url: /config/websockets + - page: Upstream Health Checks + url: /config/health-checks + - page: Client Authorization + url: /config/client-authorization + - page: TLS Delegation + url: /config/tls-delegation + - page: Rate Limiting + url: /config/rate-limiting + - page: Access logging + url: /config/access-logging + - page: Cookie Rewriting + url: /config/cookie-rewriting + - page: Overload Manager + url: /config/overload-manager + - page: JWT Verification + url: /config/jwt-verification + - page: IP Filtering + url: /config/ip-filtering + - page: Annotations Reference + url: /config/annotations + - page: Slow Start Mode + url: /config/slow-start + - page: Tracing Support + url: /config/tracing + - page: API Reference + url: /config/api + - title: Deployment + subfolderitems: + - page: Deployment Options + url: /deploy-options + - page: Contour Configuration + url: /configuration + - page: Upgrading Contour + link: /resources/upgrading + - page: Enabling TLS between Envoy and Contour + url: /grpc-tls-howto + - page: Redeploy Envoy + url: /redeploy-envoy + - title: Guides + subfolderitems: + - page: Deploying Contour on AWS with NLB + url: /guides/deploy-aws-nlb/ + - page: AWS Network Load Balancer TLS Termination with Contour + url: /guides/deploy-aws-tls-nlb/ + - page: Deploying HTTPS services with Contour and cert-manager + url: /guides/cert-manager/ + - page: External Authorization Support + url: /guides/external-authorization/ + - page: FIPS 140-2 in Contour + url: /guides/fips + - page: Using Gatekeeper with Contour + url: /guides/gatekeeper + - page: Using Gateway API with Contour + url: /guides/gateway-api + - page: Global Rate Limiting + url: /guides/global-rate-limiting + - page: Configuring ingress to gRPC services with Contour + url: /guides/grpc + - page: Health Checking + url: /guides/health-checking + - page: Creating a Contour-compatible kind cluster + url: /guides/kind + - page: Collecting Metrics with Prometheus + url: /guides/prometheus/ + - page: How to Configure PROXY Protocol v1/v2 Support + url: /guides/proxy-proto/ + - page: Contour/Envoy Resource Limits + url: /guides/resource-limits/ + - title: Troubleshooting + subfolderitems: + - page: Troubleshooting Common Proxy Errors + url: /troubleshooting/common-proxy-errors + - page: Envoy Administration Access + url: /troubleshooting/envoy-admin-interface + - page: Contour Debug Logging + url: /troubleshooting/contour-debug-log + - page: Envoy Debug Logging + url: /troubleshooting/envoy-debug-log + - page: Visualize the Contour Graph + url: /troubleshooting/contour-graph + - page: Show Contour xDS Resources + url: /troubleshooting/contour-xds-resources + - page: Profiling Contour + url: /troubleshooting/profiling-contour + - page: Envoy Container Stuck in Unready State + url: /troubleshooting/envoy-container-draining + - title: Resources + subfolderitems: + - page: Support Policy + link: /resources/support + - page: Compatibility Matrix + link: /resources/compatibility-matrix + - page: Contour Deprecation Policy + link: /resources/deprecation-policy + - page: Release Process + link: /resources/release-process + - page: Frequently Asked Questions + link: /resources/faq + - page: Tagging + link: /resources/tagging + - page: Adopters + link: /resources/adopters + - page: Ecosystem + link: /resources/ecosystem + - title: Security + subfolderitems: + - page: Threat Model and Security Posture + link: /resources/security-threat-model + - page: Security Report Process + link: /resources/security-process + - page: Security Fix Checklist + link: /resources/security-checklist + - title: Contribute + subfolderitems: + - page: Start Contributing + url: /start-contributing + - page: How We Work + link: /resources/how-we-work diff --git a/site/data/docs/toc-mapping.yml b/site/data/docs/toc-mapping.yml index 4efb5bf2424..d1ccf6597f8 100644 --- a/site/data/docs/toc-mapping.yml +++ b/site/data/docs/toc-mapping.yml @@ -51,3 +51,4 @@ v1.19.1: v1-19-1-toc "1.26": 1-26-toc "1.27": 1-27-toc "1.28": 1-28-toc +"1.29": 1-29-toc diff --git a/versions.yaml b/versions.yaml index 47f76dd4d36..51bdd69e37d 100644 --- a/versions.yaml +++ b/versions.yaml @@ -14,6 +14,16 @@ versions: - "1.27" gateway-api: - "1.0.0" + - version: v1.29.0 + supported: "true" + dependencies: + envoy: "1.30.1" + kubernetes: + - "1.29" + - "1.28" + - "1.27" + gateway-api: + - "1.0.0" - version: v1.28.4 supported: "true" dependencies: @@ -105,7 +115,7 @@ versions: gateway-api: - "0.8.1" - version: v1.26.3 - supported: "true" + supported: "false" dependencies: envoy: "1.27.4" kubernetes: